One ViewModel per View? - wpf

I am developing a UI application which displays 3 datagrids, each of them are dependent on each other(selectedItem binding)
This is what I have done so far:
datagrid1 has one Model and one ViewModel,
datagrid2 has one Model and one ViewModel,
datagrid3 has one Model and one ViewModel.
and all three ViewModels are mapped to the View using DataContext in each <Datagrid.DataContext> property. The reason for setting ViewModel for each model is, The datagrid has to select the items from their respective tables from the database.
But now I am facing a difficulty when I try to set the SelectedItem in each datagrid.
I am pasting a sample code for one datagrid. Like this I have created viewmodel for other two datagrid's also.
XAML
<DataGrid DataContext="{Binding Path=HostData,NotifyOnTargetUpdated=True,Mode=OneWay}"
AutoGenerateColumns="False" Name="hostDatagrid" Margin="171,32,235,230">
<Datagrid.DataContext>
<host:HostViewModel>
</Datagrid.DataContext>
<DataGrid.Columns>
<DataGridTextColumn Header="Host" Width="auto" Binding="{Binding HostID}" />
<DataGridTextColumn Header="Status" Width="auto" Binding="{Binding HostStatus}"/>
</DataGrid.Columns>
</DataGrid>
<DataGrid DataContext="{Binding Path=LogData,NotifyOnTargetUpdated=True,Mode=OneWay}"
AutoGenerateColumns="False" Name="LogDatagrid" Margin="103,108,102,145">
<Datagrid.DataContext>
<host:LogViewModel>
</Datagrid.DataContext>
<DataGrid.Columns>
<DataGridTextColumn Header="Host ID" Width="auto" Binding="{Binding HostID}" />
<DataGridTextColumn Header="Logs" Width="auto" Binding="{Binding LogID}" />
<DataGridTextColumn Header="Log Path" Width="auto" Binding="{Binding LogPath}"/>
<DataGridTextColumn Header="Date" Width="auto" Binding="{Binding Date}"/>
<DataGridTextColumn Header="Last Activity" Width="auto" Binding="{Binding LastActivity}"/>
c#- Model
public LogFileModel()
{
}
private int _hostID;
public int HostID
{
get { return _hostID; }
set { _hostID= value; OnpropertyChanged("HostID"); }
}
private string _logid;
public string LogID
{
get { return _logid; }
set { _logid= value; OnpropertyChanged("LogID"); }
}
private string _logpath;
public string LogPath
{
get { return _logPath; }
set { _logPath = value; OnpropertyChanged("LogPath"); }
}
private DateTime _date;
public DateTime Date;
{
get { return _date; }
set { _date= value; OnpropertyChanged("Date"); }
}
private bool _activity;
public bool LastActivity
{
get { return _activity; }
set { _activity= value; OnpropertyChanged("LastActivity"); }
}
ViewModel
LogModel _myModel = new LogModel();
private ObservableCollection<LogFileModel> _logFileData = new
ObservableCollection<LogFileModel>();
public ObservableCollection<LogFileModel> LogFileData
{
get { return _logFileData; }
set { _logFileData = value; OnPropertyChanged("LogFileData"); }
} public LogFileViewModel()
{
initializeload();
timer.Tick += new EventHandler(timer_Tick);
timer.Interval = new TimeSpan(0, 0, 3);
timer.Start();
}
~LogFileViewModel()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
timer.Stop();
timer.Tick -= new EventHandler(timer_Tick);
}
disposed = true;
}
}
private void timer_Tick(object sender, EventArgs e)
{
try
{
LogFileData.Clear();
initializeload();
}
catch (Exception ex)
{
timer.Stop();
Console.WriteLine(ex.Message);
}
}
private void initializeload()
{
try
{
DataTable table = _myModel.getData();
for (int i = 0; i < table.Rows.Count; ++i)
LogFileData.Add(new LogFileModel
{
HostID= Convert.ToInt32(table.Rows[i][0]),
LogID = table.Rows[i][1].ToString(),
LogPath = table.Rows[i][2].ToString(),
Date = Convert.ToDateTime(table.Rows[i][3]),
LastAcivity= table.Rows[i][4].ToString(),
});
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyname)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyname));
}
public class LogModel
{
public DataTable getData()
{
DataTable ndt = new DataTable();
SqlConnection sqlcon = new SqlConnection(ConfigurationManager.ConnectionStrings["MyConnectionString"].ConnectionString);
sqlcon.Open();
//for this select statement only I have created separate ViewModel for the respective Model
SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [LocalDB].[dbo].[LogFiles]", sqlcon);
da.Fill(ndt);
da.Dispose();
sqlcon.Close();
return ndt;
}
}
}
Is there any other way to overcome this issue and to have one ViewModel for one View, with their respective datagrid - select statements?

You are always free to split your view in different sub-views (like user controls).
The same thing you can do with the ViewModels too.
You can create one ViewModel which represents the whole view knowing the three sub viewmodels...
public class MainViewModel : ViewModel
{
private LogViewModel _SubLogViewModel = new LogViewModel();
public LogViewModel SubLogViewModel
{
get
{
return _SubLogViewModel;
}
set
{
if (_SubLogViewModel != value)
{
_SubLogViewModel = value;
OnpropertyChanged("SubLogViewModel");
}
}
}
private HostViewModel _SubHostViewModel = new HostViewModel();
public HostViewModel SubHostViewModel
{
get
{
return _SubHostViewModel;
}
set
{
if (_SubHostViewModel != value)
{
_SubHostViewModel = value;
OnpropertyChanged("SubHostViewModel");
}
}
}
private Host _SelectedHost;
public Host SelectedHost
{
get
{
return _SelectedHost;
}
set
{
if (_SelectedHost!= value)
{
_SelectedHost= value;
OnpropertyChanged("SelectedHost");
if(this.SelectedHost != null && this.SubLogViewModel != null)
{
this.SubLogViewModel.LoadLogFor(this.SelectedHost);
}
}
}
}
}
and the XAML something like:
<Grid>
<Grid.DataContext>
<host:MainViewModel />
</Grid.DataContext>
<DataGrid Name="hostDataGrid" DataContext="{Binding SubHostModel}" SelectedItem="{Binding SelectedHost, Mode=TwoWay}">
...
</DataGrid>
<DataGrid Name="LogDatagrid" DataContext="{Binding SubLogModel}">
</DataGrid>
</Grid>

Related

Data after Saving not showing on DataGrid in WPF

This is my service in MVVM. The data is added to the list and can see it in debugger but just before showing it on UI Presentation it did not show. i had applied two way binding and have use DataGridView. its showing data on the LoadData function but not loading. i am starter to the WPF
public EmployeeViewModel()
{
CurrentEmployee = new Employee();
objEmployeeService = new EmployeeService();
saveCommand = new RelayCommand(Save);
LoadData();
}
private void LoadData()
{
obj_Employee = new ObservableCollection<Employee>(objEmployeeService.GetAllEmployees());
}
#endregion
#region AddEmployee
private Employee currentEmployee;
private string message;
public string Message { get { return message; } set { message = value; OnPropertyChanged("Message"); } }
public Employee CurrentEmployee
{
get { return currentEmployee; }
set { currentEmployee = value; OnPropertyChanged("CurrentEmployee"); }
}
private RelayCommand saveCommand;
public RelayCommand SaveCommand
{
get { return saveCommand; }
}
public void Save()
{
bool IsSaved=false;
try
{
IsSaved = objEmployeeService.Add(CurrentEmployee);
LoadData();
}
catch (Exception ex)
{
Message = ex.Message;
}
if (IsSaved)
Message = "Employee Saved";
}
Here is my UI
<DataGrid x:Name="dgridEmployees"
AutoGenerateColumns="False"
Grid.Row="6"
Grid.Column="1"
Margin="3"
ItemsSource="{Binding Path = Employees, Mode=TwoWay}"
>
<DataGrid.Columns>
<DataGridTextColumn Header="EmployeeID"
Width="auto"
Binding=
"{Binding Path=Id}"></DataGridTextColumn>
<DataGridTextColumn Header="Employee Name"
Width="auto"
Binding=
"{Binding Path=Name}"></DataGridTextColumn>
<DataGridTextColumn Header="EmployeeAge"
Width="auto"
Binding=
"{Binding Path=Age}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
It does not appear that this binding goes anywhere...what is Employees>
ItemsSource="{Binding Path = Employees, Mode=TwoWay}"
You only show obj_Employee being set. Maybe bind to that?
As to a re-load, ...sometimes the control needs to be signaled to announce a change after an initial bind. This is sometimes done by setting the initial binding target to null first, and then set to the new reference. Add this into your load:
obj_Employee = null;
obj_Employee = new ObservableCollection<Employee>(objEmployeeService.GetAllEmployees());

WPF: Update cell value in row of main DataGrid when sum of values in column of sub DataGrid change

I have a main DataGrid (bound to ObservableCollection<MainItem>) with another sub DataGrid defined in the RowDetailsTemplate (bound to ObservableCollection<SubItem>). The MainItem.Total property is just a derived value that returns SubItems.Sum(s => s.Amount). This works great upon displaying the grids.
Yet, when a value in the Amount column is changed and SubItem.Amountupdated, though the MainItem.Total value is correct (it is just a derived value), the main grid doesn't refresh to show the new value. If I force MainDataGrid.Items.Refresh() (for example, in MainDataGrid_SelectedCellsChanged), the main grid then displays the value now in MainItem.Total. So, this works but it smells since it is such a brute force method refreshing all visible rows in the main grid. [NOTE: This situation is similar to the article Update single row in a WPF Datagrid but I'm already using an ObservableCollection.]
I expect this behavior is because the change occurred in ObservableCollection<SubItem> not in ObservableCollection<MainItem> so it doesn't know about it? But I haven't found an event to raise that informs the main grid that the contents of the bound MainItem.Total has been updated.
[NOTE: The code below is a self-contained sample in case someone wants to build this and try it. It is based on the same situation in my real project.]
XAML:
<Window x:Class="WpfAppDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfAppDataGrid"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<DataGrid x:Name="MainDataGrid" Grid.Column="0"
ItemsSource="{Binding MainItems}"
RowDetailsVisibilityMode="VisibleWhenSelected"
SelectedCellsChanged="MainDataGrid_SelectedCellsChanged"
AutoGenerateColumns="False"
CanUserSortColumns="True"
CanUserAddRows="True"
CanUserDeleteRows="True"
RowBackground="AliceBlue"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="10,10,10,10">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid x:Name="SubDataGrid" ItemsSource="{Binding SubItems}"
CellEditEnding="SubDataGrid_CellEditEnding"
AutoGenerateColumns="False"
CanUserAddRows="True"
CanUserDeleteRows="True"
RowBackground="Bisque">
<DataGrid.Columns>
<DataGridTextColumn Header="Candidate" Binding="{Binding Candidate}" />
<DataGridTextColumn Header="Primary" Binding="{Binding Primary}" />
<DataGridTextColumn Header="Amount" Binding="{Binding Amount, StringFormat=\{0:N2\}}" />
<DataGridTextColumn Header="Previous" Binding="{Binding Previous}" />
<DataGridTextColumn Header="Party" Binding="{Binding Party}" />
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid.Columns>
<DataGridTextColumn Header="Voter" Binding="{Binding Voter}" />
<DataGridTemplateColumn Header="Date" Width="100" SortMemberPath="Date" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding RecordedDate, StringFormat=\{0:MM/dd/yyyy\}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding RecordedDate}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" />
<DataGridTextColumn Header="Auto" Binding="{Binding Total, StringFormat=\{0:N2\}}" IsReadOnly="True" >
<DataGridTextColumn.CellStyle>
<Style>
<Setter Property="TextBlock.TextAlignment" Value="Right" />
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<StackPanel Grid.Column="1" Margin="10,10,10,10">
<Button Content="Refresh" Click="Button_Click" />
</StackPanel>
</Grid>
</Window>
View code-behind:
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WpfAppDataGrid
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
enum SubDataGridColumn
{
Candidate,
Primary,
Amount,
Previous,
Party
}
private static MainViewModel vm = new MainViewModel();
private static bool subAmountChanged = false;
public MainWindow()
{
DataContext = vm;
InitializeComponent();
}
private void SubVerifyEdit(SubItem sub, int column, string txt)
{
switch ((SubDataGridColumn)column)
{
case SubDataGridColumn.Candidate:
if (sub.Candidate != txt)
sub.Candidate = txt;
break;
case SubDataGridColumn.Primary:
if (sub.Primary != txt)
sub.Primary = txt;
break;
case SubDataGridColumn.Amount:
var amount = decimal.Parse(txt);
if (sub.Amount != amount)
{
sub.Amount = amount;
subAmountChanged = true;
}
break;
case SubDataGridColumn.Party:
if (sub.Party != txt)
sub.Primary = txt;
break;
case SubDataGridColumn.Previous:
if (sub.Previous != txt)
sub.Previous = txt;
break;
default:
break;
}
}
private void SubDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
var sub = (SubItem)e.Row.Item;
var column = e.Column.DisplayIndex;
var dep = (DependencyObject)e.EditingElement;
if (dep is TextBox)
{
SubVerifyEdit(sub, column, ((TextBox)dep).Text);
}
else if (dep is ComboBox)
{
SubVerifyEdit(sub, column, ((ComboBox)dep).SelectedItem.ToString());
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MainDataGrid.Items.Refresh();
}
private void MainDataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
if (subAmountChanged)
{
//var main = (MainItem)MainDataGrid.CurrentItem;
//var sum = main.SubItems.Sum(s => s.Amount);
//var manual = main.ManualTotal;
//var auto = main.AutoTotal;
//var dep = (DependencyProperty)MainDataGrid.CurrentItem;
//MainDataGrid.CoerceValue(dep);
MainDataGrid.Items.Refresh();
subAmountChanged = false;
}
}
}
}
ViewModel:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
namespace WpfAppDataGrid
{
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MainViewModel()
{
MainItems = new ObservableCollection<MainItem>();
MainItems.Add(new MainItem("Thomas", DateTime.Now, "Unsure"));
MainItems[0].SubItems.Add(new SubItem("Roberts", "NH", 27.46m, "Senator", "Republican"));
MainItems.Add(new MainItem("Arthur", DateTime.Now, "No worries"));
MainItems[1].SubItems.Add(new SubItem("Johnson", "IA", 47.5m, "Representative", "Republican"));
MainItems[1].SubItems.Add(new SubItem("Butegieg", "CA", 76.42m, "Senator", "Democrat"));
MainItems[1].SubItems.Add(new SubItem("Warren", "SC", 14.5m, "Governor", "Democrat"));
MainItems.Add(new MainItem("Cathy", DateTime.Now, "What now"));
MainItems[2].SubItems.Add(new SubItem("Biden", "WI", 1456.98m, "Mayor", "Democrat"));
MainItems.Add(new MainItem("Jonathan", DateTime.Now, "Got this"));
MainItems[3].SubItems.Add(new SubItem("Foobar", "MI", 5672.3m, "None", "Republican"));
MainItems[3].SubItems.Add(new SubItem("Sanders", "ME", 1.45m, "Senator", "Democrat"));
MainItems.Add(new MainItem("Roger", DateTime.Now, "Still undecided"));
MainItems[4].SubItems.Add(new SubItem("Wakemeyer", "AK", 56m, "Police Chief", "Democrat"));
MainItems[4].SubItems.Add(new SubItem("Trump", "FL", 982.34m, "Businessman", "Republican"));
}
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<MainItem> mainItems;
public ObservableCollection<MainItem> MainItems
{
get { return mainItems; }
set
{
mainItems = value;
NotifyPropertyChanged();
}
}
}
}
MainItem class:
using System;
using System.Collections.ObjectModel;
using System.Linq;
namespace WpfAppDataGrid
{
public class MainItem
{
public MainItem() { }
public MainItem(string voter, DateTime date, string status)
{
Voter = voter;
RecordedDate = date;
Status = status;
}
public string Voter { get; set; }
public DateTime RecordedDate { get; set; }
public string Status { get; set; }
public decimal Total { get { return SubItems.Sum(s => s.Amount); } }
public ObservableCollection<SubItem> SubItems { get; set; } = new ObservableCollection<SubItem>();
}
}
SubItem class:
namespace WpfAppDataGrid
{
public class SubItem
{
public SubItem() { }
public SubItem(string candidate, string primary, decimal amount, string previous, string party)
{
Candidate = candidate;
Primary = primary;
Amount = amount;
Previous = previous;
Party = party;
}
public string Candidate { get; set; }
public string Primary { get; set; }
public decimal Amount { get; set; }
public string Previous { get; set; }
public string Party { get; set; }
}
}
Both MainItem and SubItem should implement INotifyPropertyChanged.
The latter should raise the PropertyChanged event whenever the Amount property is changed and the MainItem class then needs to subscribe to the PropertyChanged event for all SubItem objects in SubItems and raise the PropertyChanged event for the Total property whenever any of them is changed:
public class MainItem : INotifyPropertyChanged
{
public MainItem()
{
SubItems.CollectionChanged += SubItems_CollectionChanged;
}
public MainItem(string voter, DateTime date, string status)
{
Voter = voter;
RecordedDate = date;
Status = status;
SubItems.CollectionChanged += SubItems_CollectionChanged;
}
private void SubItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object subItem in e.NewItems)
{
(subItem as INotifyPropertyChanged).PropertyChanged
+= new PropertyChangedEventHandler(item_PropertyChanged);
item_PropertyChanged(sub, new PropertyChangedEventArgs(nameof(Total)));
}
}
if (e.OldItems != null)
{
foreach (object country in e.OldItems)
{
item_PropertyChanged(sub, new PropertyChangedEventArgs(nameof(Total)));
(subItem as INotifyPropertyChanged).PropertyChanged
-= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyPropertyChanged(e.PropertyName);
}
public string Voter { get; set; }
public DateTime RecordedDate { get; set; }
public string Status { get; set; }
public decimal Total { get { return SubItems.Sum(s => s.Amount); } }
public ObservableCollection<SubItem> SubItems { get; set; } = new ObservableCollection<SubItem>();
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

Updating a collection member's property within another collection to show in a DataGrid

How can I make it so when I change a property of a collection member in a collection it updates in the datagrid display?
In my example I have an ObservableCollection of Employees. Each employee has a List property.
When I assign the Employee's Car value to a new List it will update successfully. When I assign the Employee's Car's Model property it doesn't update.
MainWindow.xaml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="DataBindings.MainWindow" Title="MainWindow" Height="332" Width="474" DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<CollectionViewSource x:Key="GridDataSource" Source="{Binding Employees}" />
</Window.Resources>
<Grid Margin="0,0,0,-1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DataGrid Grid.Row="1" ItemsSource="{Binding Source={StaticResource GridDataSource}}" AutoGenerateColumns="False" CanUserAddRows="False" Margin="0,0,0,114">
<DataGrid.Columns>
<DataGridTextColumn x:Name="Names" Width="Auto" Header="Name" Binding="{Binding Name}" />
<DataGridTextColumn x:Name="Cars" Width="Auto" Header="Cars" Binding="{Binding CarsString}" />
</DataGrid.Columns>
</DataGrid>
<Button Content="Update" Click="Update" HorizontalAlignment="Left" Margin="170,202,0,-20" Grid.Row="1" VerticalAlignment="Top" Width="75" />
</Grid>
MainWindow.xaml.cs
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
namespace DataBindings
{
public partial class MainWindow : Window
{
DAL dataAccess = new DAL();
public MainWindow()
{
InitializeComponent();
}
public ObservableCollection<Employee> Employees { get { return dataAccess.EmployeesList; } }
private void Update(object sender, RoutedEventArgs e)
{
Employees[1].Name = "Mike"; //changing the name property on the collection of employees works
Debug.WriteLine($"Mike's Car is a {Employees[1].Cars[0].Model}");
Employees[1].Cars[0].Model = "Volvo"; //changing the model of car in a cars list does not
Debug.WriteLine($"Mike's Car is a {Employees[1].Cars[0].Model}");
}
}
}
DAL.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace DataBindings
{
public class DAL : ObservableCollection<Employee>
{
public ObservableCollection<Employee> EmployeesList = new ObservableCollection<Employee>();
public DAL()
{
List<Car> carList = new List<Car>();
carList.Add(new Car { Model = "Ford" });
carList.Add(new Car { Model = "Honda" });
EmployeesList.Add(new Employee { Name = "Bob", Cars = carList });
EmployeesList.Add(new Employee { Name = "John", Cars = carList });
//EmployeesList.CollectionChanged += EmployeesList_CollectionChanged;
}
}
public class Employee : INotifyPropertyChanged
{
private string empName;
public List<Car> empCars = new List<Car>();
private string carsString;
public string Name
{
get { return this.empName; }
set
{
if (this.empName != value)
{
empName = value;
this.NotifyPropertyChanged("Name");
};
}
}
public List<Car> Cars
{
get { return this.empCars; }
set
{
if (this.empCars != value)
{
empCars = value;
carsToString(empCars);
this.NotifyPropertyChanged("Cars");
};
}
}
public string CarsString
{
get { return this.carsString; }
set
{
if (this.carsString != value)
{
carsString = value;
this.NotifyPropertyChanged("CarsString");
};
}
}
public void carsToString(List<Car> Cars)
{
string carString = "";
foreach (Car car in Cars)
{
carString += car.Model + " ";
}
CarsString = carString;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public class Car : INotifyPropertyChanged
{
private string carModel;
public string Model
{
get { return this.carModel; }
set
{
if (this.carModel != value)
{
carModel = value;
this.NotifyPropertyChanged("Model");
};
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
I modified your code a bit, which IMO can be improved more.
Here is my xaml:
<Window.DataContext>
<local:DAL />
</Window.DataContext>
<Grid Margin="0,0,0,-1">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DataGrid x:Name="grid" Grid.Row="1" ItemsSource="{Binding EmployeesList}" AutoGenerateColumns="False" CanUserAddRows="False" Margin="0,0,0,114">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Names">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Cars">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CarsString, Mode=TwoWay}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button Content="Update" Click="Update" HorizontalAlignment="Left" Margin="170,202,0,-20" Grid.Row="1" VerticalAlignment="Top" Width="75" />
</Grid>
Here's my code-behind:
You can have an ICommand to your UpdateButton so you can leave your code behind clean: https://www.c-sharpcorner.com/UploadFile/e06010/wpf-icommand-in-mvvm/
public DAL VM => (DAL) DataContext;
public MainWindow()
{
InitializeComponent();
}
private void Update(object sender, RoutedEventArgs e)
{
VM.EmployeesList[1].Name = "Mike"; //changing the name property on the collection of employees works
Debug.WriteLine($"Mike's Car is a {VM.EmployeesList[1].Cars[0].Model}");
VM.EmployeesList[1].Cars[0].Model = "Volvo"; //changing the model of car in a cars list does not
Debug.WriteLine($"Mike's Car is a {VM.EmployeesList[1].Cars[0].Model}");
}
Here's my DAL.cs
Notice that I changed your List to ObservableCollection then in accessors, you need to make sure that CarsString is updated, since you're not directly updating it in your view.
public class DAL : INotifyPropertyChanged
{
private ObservableCollection<Employee> _employeesList;
public ObservableCollection<Employee> EmployeesList
{
get => _employeesList;
set
{
_employeesList = value;
OnPropertyChanged();
}
}
public DAL()
{
EmployeesList = new ObservableCollection<Employee>();
ObservableCollection<Car> carList = new ObservableCollection<Car>();
carList.Add(new Car { Model = "Ford" });
carList.Add(new Car { Model = "Honda" });
EmployeesList.Add(new Employee { Name = "Bob", Cars = carList });
EmployeesList.Add(new Employee { Name = "John", Cars = carList });
//EmployeesList.CollectionChanged += EmployeesList_CollectionChanged;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Employee : INotifyPropertyChanged
{
private string empName;
public ObservableCollection<Car> empCars = new ObservableCollection<Car>();
private string carsString;
public string Name
{
get { return this.empName; }
set
{
if (this.empName != value)
{
empName = value;
this.NotifyPropertyChanged("Name");
};
}
}
public ObservableCollection<Car> Cars
{
get
{
carsToString(empCars);
this.NotifyPropertyChanged("CarsString");
return this.empCars;
}
set
{
if (this.empCars != value)
{
empCars = value;
carsToString(empCars);
this.NotifyPropertyChanged("Cars");
this.NotifyPropertyChanged("CarsString");
};
}
}
public string CarsString
{
get { return this.carsString; }
set
{
if (this.carsString != value)
{
carsString = value;
this.NotifyPropertyChanged("CarsString");
};
}
}
public void carsToString(ObservableCollection<Car> Cars)
{
string carString = "";
foreach (Car car in Cars)
{
carString += car.Model + " ";
}
CarsString = carString;
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public class Car : INotifyPropertyChanged
{
private string carModel;
public string Model
{
get { return this.carModel; }
set
{
if (this.carModel != value)
{
carModel = value;
this.NotifyPropertyChanged("Model");
};
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}

WPF Datagrid not losing focus when a nested item is clicked

I have a datagrid with a template column that nests another datagrid. When I edit a person's name on the main datagrid and then double click to edit a nested item, the main one doesn't lose focus so that the cell remains in editing mode.
Here is the picture.
I deal with it by registering GotFocus event for the nested datagrid, finding parent datagrid in visual tree using a custom FindAncestor function and invoking CancelEdit() on the main datagrid.
private void DataGridItem_GotFocus(object sender, RoutedEventArgs e)
{
var dg =(DataGrid)FindAncestor((DependencyObject)sender, typeof(DataGrid), 2);
dg.CancelEdit();
}
That is rather an overkill. Is there a different, maybe a more MVVM-esque way to tackle this issue?
Below is the full code of this simplified example.
XAML
<Grid>
<DataGrid x:Name="DataGrid1"
ItemsSource="{Binding DataCollection}"
SelectedItem="{Binding DataCollectionSelectedItem}"
AutoGenerateColumns="False"
CanUserAddRows="false" >
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="LightBlue"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="2*" />
<DataGridTemplateColumn Header="Item/Price" Width="3*">
<DataGridTemplateColumn.CellTemplate >
<DataTemplate>
<DataGrid x:Name="DataGridItem"
ItemsSource="{Binding Items}"
SelectedItem="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ItemsSelectedItem}"
GotFocus="DataGridItem_GotFocus"
Background="Transparent"
HeadersVisibility="None"
AutoGenerateColumns="False"
CanUserAddRows="false" >
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding ItemName}" Width="*"/>
<DataGridTextColumn Binding="{Binding Price}" Width="50"/>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
C#
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace datagrid_focus
{
public class Item : NotifyObject
{
private string _itemName;
public string ItemName
{
get { return _itemName; }
set { _itemName = value; OnPropertyChanged("ItemName"); }
}
private double _price;
public double Price
{
get { return _price; }
set { _price = value; OnPropertyChanged("Price"); }
}
}
public class Person : NotifyObject
{
public ObservableCollection<Item> Items { get; set; }
private string _name;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged("Name"); }
}
public double Total
{
get { return Items.Sum(i => i.Price); }
set { OnPropertyChanged("Total"); }
}
}
public abstract class NotifyObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public class ViewModel : NotifyObject
{
public ObservableCollection<Person> DataCollection { get; set; }
public Person DataCollectionSelectedItem { get; set; }
public Item ItemsSelectedItem { get; set; }
public ViewModel()
{
DataCollection = new ObservableCollection<Person>
{
new Person {Name = "Siegmund Freud", Items = new ObservableCollection<Item> {
new Item { ItemName = "Phone", Price = 220 },
new Item { ItemName = "Tablet", Price = 350 },
} },
new Person {Name = "Karl Popper", Items = new ObservableCollection<Item> {
new Item { ItemName = "Teddy Bear Deluxe", Price = 2200 },
new Item { ItemName = "Pokemon", Price = 100 }
}}
};
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
public DependencyObject FindAncestor(DependencyObject current, Type type, int levels)
{
int currentLevel = 0;
while (current != null)
{
if (current.GetType() == type)
{
currentLevel++;
if (currentLevel == levels)
{
return current;
}
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
private void DataGridItem_GotFocus(object sender, RoutedEventArgs e)
{
var dg =(DataGrid)FindAncestor((DependencyObject)sender, typeof(DataGrid), 2);
//dg.CancelEdit();
}
}
}

WPF DataGrid with DataGrid in RowDetailsTemplate

My previous post about detecting property changes in the VM wasn't in depth enough, so I'm posting this
I have a grid of Jobs. Each job can have one or more employees.
The DataGrid's RowDetailsTemplate contains another grid to show the employees. So to parent grid is bound to a list of Jobs. The inner grid is bound to a list of Employees that is on the Job model.
The Job Model:
public class Job : _Base
{
private string _JobName = string.Empty;
public string JobName
{
get { return _JobName; }
set
{
if (_JobName != value)
{
_JobName = value;
RaisePropertyChanged("JobName");
}
}
}
private string _JobNumber = string.Empty;
public string JobNumber
{
get { return _JobNumber; }
set
{
if (_JobNumber != value)
{
_JobNumber = value;
RaisePropertyChanged("JobNumber");
}
}
}
private ObservableCollection<Employee> _Employees;
public ObservableCollection<Employee> Employees
{
get { return _Employees; }
set
{
if (_Employees != value)
{
if (_Employees != value)
{
_Employees = value;
RaisePropertyChanged("Employees");
}
}
}
}
private Employee _SelectedEmployee;
public Employee SelectedEmployee
{
get { return _SelectedEmployee; }
set
{
if (_SelectedEmployee != value)
{
if (_SelectedEmployee != value)
{
_SelectedEmployee = value;
RaisePropertyChanged("SelectedEmployee");
}
}
}
}
public Job()
{
Employees = new ObservableCollection<Employee>();
}
}
The Employee model
public class Employee : _Base
{
private string _EmployeeName = string.Empty;
public string EmployeeName
{
get { return _EmployeeName; }
set
{
if (_EmployeeName != value)
{
_EmployeeName = value;
RaisePropertyChanged("EmployeeName");
}
}
}
private bool _IsChecked = false;
public bool IsChecked
{
get { return _IsChecked; }
set
{
if (_IsChecked != value)
{
_IsChecked = value;
RaisePropertyChanged("IsChecked");
}
}
}
}
The XAML
<DataGrid ItemsSource="{Binding Jobs}"
SelectedItem="{Binding SelectedJob}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Job Name" Binding="{Binding JobName}" />
<DataGridTextColumn Header="Job Number" Binding="{Binding JobNumber}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<DataGrid ItemsSource="{Binding Employees}"
SelectedItem="{Binding SelectedEmployee}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsChecked}"/>
<DataGridTextColumn Binding="{Binding EmployeeName}"/>
</DataGrid.Columns>
</DataGrid>
<Button Margin="5"
Height="23"
Width="75"
HorizontalAlignment="Left"
Content="Remove"/>
</StackPanel>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
The MainWindowViewModel
public class MainWindowViewModel : _Base
{
private ObservableCollection<Job> _Jobs;
public ObservableCollection<Job> Jobs
{
get { return _Jobs; }
set
{
if (_Jobs != value)
{
if (_Jobs != value)
{
_Jobs = value;
RaisePropertyChanged("Jobs");
}
}
}
}
private Job _SelectedJob;
public Job SelectedJob
{
get { return _SelectedJob; }
set
{
if (_SelectedJob != value)
{
if (_SelectedJob != value)
{
_SelectedJob = value;
RaisePropertyChanged("SelectedJob");
}
}
}
}
public MainWindowViewModel()
{
this.PropertyChanged += new PropertyChangedEventHandler(MainWindowViewModel_PropertyChanged);
}
void MainWindowViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName.Trim().ToLower() == "ischecked")
{
int x = 1;
}
}
}
I have a couple of questions:
1) The SelectedEmployee property on the Job model does not fire when I click an employee in the inner grid.
2) The MainWindowViewModel_PropertyChanged does not fire when an employee is selected.
3) Notice the button below the inner grid. How do I bind its command to MainWindowVM?
As you have DataGridinside DataGrid's row, so the above DataGrid is somehow eating up the selectionchange for the inner DataGrid. To solve this, you will need to forcefully update the binding source for the child DataGrid. For doing so capture the SelectionChanged event for the inner DataGrid and in the handler do the following.
private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DataGrid grid = e.OriginalSource as DataGrid;
var expression = grid.GetBindingExpression(DataGrid.SelectedItemProperty);
expression.UpdateSource();
}
MainwindowVM does not have any ischecked property thats why its propertychanged for that property is not firing.
To solve this you need to do couple of things. The problem is DataGrid cell and rows does not inherit the DataContext as they don't come under its visual tree. So to solve this you will have to use the BindingProxy to take the windows DataContext to your button in rowdetails of DataGrid
Define Binding Proxy class as below:
public class MyBindingProxy : Freezable
{
public static readonly DependencyProperty BindingDataProperty =
DependencyProperty.Register("BindingData", typeof(object),
typeof(MyBindingProxy), new UIPropertyMetadata(null));
protected override Freezable CreateInstanceCore()
{
return new MyBindingProxy();
}
public object BindingData
{
get { return (object)GetValue(BindingDataProperty); }
set { SetValue(BindingDataProperty, value); }
}
}
Now in the resources of your window (where DataGrid is) create the instance of proxy and set the BindingData to the DataContext of the Window i.e MainWindowViewModel as:
<Window.Resources>
<local:MyBindingProxy x:Key="myproxy" BindingData="{Binding}" />
</Window.Resources>
Now just set command as below on the button:
<Button Margin="5"
Height="23"
Width="75"
HorizontalAlignment="Left"
Content="Remove"
Command="{Binding BindingData.MyCommand, Source={StaticResource myproxy}}"/>

Resources