I have this simple example program with 2 columns. The column on the left holds a datagrid, and the column on the right has a red rectangle in the middle. I want to make a binding, so that the width/height of the rectangle is always equal to the size of the biggest item in the list.
Picture of Main Window:
Picture of Solution explorer:
So far i have this:
MainWindow.xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!-- Left Column -->
<StackPanel Margin="0 20 0 0">
<DataGrid ItemsSource="{Binding Items}" Margin="20 0 20 0"
AutoGenerateColumns="False"
CanUserResizeColumns="False" CanUserResizeRows="False"
CanUserSortColumns="False" CanUserReorderColumns="False"
CanUserDeleteRows="False" CanUserAddRows="False"
SelectionMode="Single" SelectionUnit="Cell">
<DataGrid.Columns>
<!-- Column 1 - Name -->
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"
Width="150">
</DataGridTextColumn>
<!-- Column 2 - Size -->
<DataGridTextColumn Header="Size"
Binding="{Binding Size}"
Width="*">
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<!-- Right Column -->
<Border Grid.Column="1"
BorderBrush="Black"
BorderThickness="1">
<Rectangle Width="{Binding SizeOfRectangle}"
Height="{Binding SizeOfRectangle}"
Fill="Red">
</Rectangle>
</Border>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
MainWindowViewModel viewModel = new MainWindowViewModel();
this.DataContext = viewModel;
InitializeComponent();
}
}
Item.cs
public class Item
{
public string Name { get; set; }
public double Size { get; set; }
}
MainWindowViewModel.cs
public class MainWindowViewModel
{
public ObservableCollection<Item> Items { get; }
public double SizeOfRectangle { get; set; }
public MainWindowViewModel()
{
//Initialize list of items:
Items = new ObservableCollection<Item>()
{
new Item { Name = "Item 1", Size = 120 },
new Item { Name = "Item 2", Size = 150 },
new Item { Name = "Item 3", Size = 180 },
new Item { Name = "Item 4", Size = 100 },
new Item { Name = "Item 5", Size = 225 }
};
//Find the biggest size and assign to "SizeOfRectangle"
double MaxSize = Items[0].Size;
for (int i = 1; i < Items.Count; i++)
{
if (Items[i].Size > MaxSize)
MaxSize = Items[i].Size;
}
SizeOfRectangle = MaxSize; //Width and Height of rectangle bound to this
}
}
I have initialized a list of 5 items, and made an iteration in the ViewModel which finds the largest size (and sets the height/width of the rectangle to 225 in this case). I want to make it so that when i change the sizes of the items through the datagrid, the program will check if the max size has changed, and the size of the rectangle can change accordingly.
I am expecting I need to make some sort of event, and make use of INotifyPropertyChanged, etc. But currently I am at a bit of a loss as to where to start. I would greatly appreciate any help you can offer.
4
No need to download a toolkit (like a different answer is suggesting), you just need to implement the interfaces. I answer the question on how to handle the data grid edit’s to Size at the bottom of my answer.
Your View Model should be implementing INotifyPropertyChanged
Then, after you set the SizeOfRectangle
you need to trigger the property changed event so that the binding will refresh. All view models must implement this interface otherwise the initial get on the vm properties will never update. After reading your question a few times it’s clear that you know this.
So add the event and these 2 helper methods after you extend the class
// implementing the INotifyPropertyChanged interface
public event PropertyChangedEventHandler PropertyChanged = delegate { };
// add some helper methods to fire the property changed.
public virtual void TriggerPropertyChanged(string propertyName)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
I always code my properties of a vm using real backend vars.. but your code (if left as-is) would just need to change your existing code like below (after you add the code I posted above):
//Find the biggest size and assign to "SizeOfRectangle"
double MaxSize = Items[0].Size;
for (int i = 1; i < Items.Count; i++)
{
if (Items[i].Size > MaxSize)
MaxSize = Items[i].Size;
}
SizeOfRectangle = MaxSize; //Width and Height of rectangle bound to this
TriggerPropertyChanged("SizeOfRectagle"); // update the view
This is how you should code vm properties going forward:
private double _Size;
public double SizeOfRectangle
{ get { return _Size; }
set
{
if (_Size != value)
{
_Size = value;
OnPropertyChanged(); //call this method without
//having to specify the property.
//The methods [CallerMemberName] will handle at compile time.
}
}
}
You are correctly using the ObserrvableCollection<Item>
however your Item class is not a “real view model”. Won’t be a problem until you start binding to properties to the Item. You should always dedicate view models and avoid using Domain level classes as view models.
To automatically update the SizeOfRectangle do the following:
Implement the INotifyPropertyChanged on Item
class and add those 2 helper methods just like you did in the MainViewModel class.
Your Item
class needs to be able to signal the SizeOfRectangle when the Size property is changed. So add a reference to the MainViewModel in the Item.
public class Item : INotifyPropertyChanged
{
...
public MainViewModel MainViewModel { get; set; }
}
So when you create an Item instance set the property (set Size
last):
new Item { Name = "Item 1", MainViewModel = this, Size = 120 }
You could always make the MainViewModel implement some interface that updates the SizeOfRectangle
if you were concerned with reuse and didn’t want to hard code a VM in the Item class, but for this example we will just use the MainViewModel itself.
Then change the Item
setter and use a backend prop:
private double _Size;
public double Size {
get {
return _Size;
}
set {
if ( _Size != value)
{
_Size = value;
if ( MainViewModel!= null && MainViewModel.SizeOfRectangle < _Size)
{
MainViewModel.SizeOfRectangle = _Size;
}
OnPropertyChanged();
}
}
}
18
In order to get notified about changes of their properties, the Item and MainViewModel class would need to implement the INotifyPropertyChanged
interface. In the following code snippet, this is done by deriving from the ObservableObject
class in the CommunityToolkit.Mvvm
package.
MainViewModel attaches a CollectionChanged
event handler to its Items
property, in which a PropertyChanged
event handler is attached to added Items and detached from removed Items.
The PropertyChanged
handler in turn notifies about a change of the MainViewModel’s MaxSize
property whenever the Size
property of an Item changes.
This implementation will update the MaxSize
property whenever
- an Item is added to the collection
- an Item is removed from the collection
- an Item’s Size property is modified
It does not cover the case where you call Items.Clear()
, which would require another switch section for case NotifyCollectionChangedAction.Reset
. It is left out here for brevity.
public partial class Item : ObservableObject
{
[ObservableProperty]
private string name;
[ObservableProperty]
private double size;
}
public partial class MainViewModel : ObservableObject
{
public ObservableCollection<Item> Items { get; } = [];
public double MaxSize => Items.Select(item => item.Size).Max();
public MainViewModel()
{
Items.CollectionChanged += OnItemsCollectionChanged;
}
private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Item item in e.NewItems)
{
item.PropertyChanged += OnItemPropertyChanged;
}
OnPropertyChanged(nameof(MaxSize));
break;
case NotifyCollectionChangedAction.Remove:
foreach (Item item in e.OldItems)
{
item.PropertyChanged -= OnItemPropertyChanged;
}
OnPropertyChanged(nameof(MaxSize));
break;
default:
break;
}
}
private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Item.Size))
{
OnPropertyChanged(nameof(MaxSize));
}
}
}
5