DataGrid record binding update

Dec 15, 2010 at 7:33 AM

Hello,

Since a while I'm trying to use Caliburn micro more and more in my projects, I even advice colleagues to use CM.  Most of the time everything works without any problems, but at the moment I'm struggling with a "simple" problem.

I've created a little sample (a Shopping cart) to show my problem. The problem is that it should be possible to change the amount for each item in the shopping cart, this action should update the total price for a specific item (See image), but it never updates the total price (Prijs)

shopping cart

Here is my (DataGrid) Xaml:

<data:DataGrid x:Name="OrderedItems" Grid.Row="0" AutoGenerateColumns="False" IsReadOnly="True" Margin="10"  >
            <data:DataGrid.Columns>
                <data:DataGridTextColumn Header="Width" Binding="{Binding Width}" />
                <data:DataGridTextColumn Header="Height" Binding="{Binding Height}" />
                <data:DataGridTemplateColumn Header="Amount" >
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBox Text="{Binding Amount, Mode=TwoWay}" cal:Message.Attach="[Event LostFocus] = [UpdateTotalprice($DataContext)]"/>
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
                <data:DataGridTextColumn Header="Item price" Binding="{Binding ItemPrice}" />
                <data:DataGridTextColumn Header="Prijs" Binding="{Binding TotalPrice}" />

                <data:DataGridTemplateColumn Header="Picture">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="No image" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>

                <data:DataGridTemplateColumn Header="">
                    <data:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Button Content="Remove" cal:Message.Attach="RemoveOrder($DataContext)" />
                        </DataTemplate>
                    </data:DataGridTemplateColumn.CellTemplate>
                </data:DataGridTemplateColumn>
            </data:DataGrid.Columns>

        </data:DataGrid>

And here my ViewModel:

public class ShoppingCartViewModel : Screen
    {

        private IObservableCollection<ShoppingCartItem> orderedItems = new BindableCollection<ShoppingCartItem>();

        public ShoppingCartViewModel()
        {
            DisplayName = "Shopping cart";
        }

        protected override void OnActivate()
        {
            orderedItems.Clear();
            orderedItems.Add(new ShoppingCartItem{Id = 1, Amount = 1, Width = 3.8m, Height = 4.1m, ItemPrice = 2.5m});
            orderedItems.Add(new ShoppingCartItem{Id = 2, Amount = 1, Width = 3.8m, Height = 4.1m, ItemPrice = 4m});
            orderedItems.Add(new ShoppingCartItem{Id = 3, Amount = 4, Width = 3.8m, Height = 4.1m, ItemPrice = 1.8m});
            orderedItems.Add(new ShoppingCartItem{Id = 4, Amount = 2, Width = 3.8m, Height = 4.1m, ItemPrice = 0.6m});
            orderedItems.Add(new ShoppingCartItem{Id = 5, Amount = 8, Width = 3.8m, Height = 4.1m, ItemPrice = 1.35m});
            NotifyOfPropertyChange(() => OrderedItems);
        }

        public IObservableCollection<ShoppingCartItem> OrderedItems
        {
            get { return orderedItems; }
        }

        public void RemoveOrder(ShoppingCartItem shoppingCartItem)
        {
            if (shoppingCartItem == null) return;

            orderedItems.Remove(shoppingCartItem);
        }

        public void UpdateTotalprice(ShoppingCartItem shoppingCartItem)
        {
            //This goed terribly wrong!
            //orderedItems = new BindableCollection<ShoppingCartItem>(orderedItems);
            NotifyOfPropertyChange(() => OrderedItems);
        }
    }

    public class ShoppingCartItem
    {
        public long Id { get; set; }
        public decimal Width { get; set; }
        public decimal Height { get; set; }
        public int Amount { get; set; }
        public decimal ItemPrice { get; set; }
        public decimal TotalPrice
        {
            get { return Amount * ItemPrice; }
        }
    }

Can somebody explain what I'm doing wrong?

 

With kind regards,

Jeroen

Dec 15, 2010 at 8:41 AM

Hi Jeroen

You'd need to implement INotifyPropertyChanged in your ShoppingCartItem

 Then in your setter for Amount you should raise the PropertyChanged event for "TotalPrice"

Then (because of the databindindings)... WPF will requery TotalPrice and display the updated value

 

i.e. whenever you change a property that you are binding to then you need to raise PropertyChanged if you want the change to be shown

 

Hope that helps

Stu

Dec 15, 2010 at 8:44 AM

Is there a reason behind the fact that ShoppingCartItem does not implement INotifyPropertyChanged?

As far as I can see, you are try to force a view update signaling that the collection property has changed... but it isn't (the reference is the same). So I dubt that the Binding engine will truly update your view as you would expect.

If ShoppingCartItem implemented INotifyPropertyChanged the total price update would be triggered by a change in either Amount or ItemPrice.

    public class ShoppingCartItem : NotifyPropertyChangedBase
    {
        private int amount;
        private int itemPrice;

        public long Id { get; set; }
        public decimal Width { get; set; }
        public decimal Height { get; set; }
        public int Amount
        {
               get { return amount; }
               set
              {
                  if (amount != value)
                  {
                       amount = value;
                       NotifyOfPropertyChange (() => Amount);
                       NotifyOfPropertyChange (() => TotalPrice);
                  }
               }
        }
        public decimal ItemPrice
        {
               get { return itemPrice; }
               set
              {
                  if (itemPrice!= value)
                  {
                       itemPrice= value;
                       NotifyOfPropertyChange (() => ItemPrice);
                       NotifyOfPropertyChange (() => TotalPrice);
                  }
               }
        }
        public decimal TotalPrice
        {
            get { return Amount * ItemPrice; }
        }
    }

Thus, the update would be immediate and should not be triggered by a FocusLost.

If you really don't want to use an 'observable' ShoppingCartItem and you need to trigger an update on LostFocus, then you need to force a CollectionChanged event (with a Reset action), instead of a PropertyChanged.

Dec 15, 2010 at 8:59 AM

Thank you both for your fast responses.

The PropertyChangedBase solution works perfect! But there is a little problem/challenge, this object (ShoppingCartItem) is gonna be a datacontract used by a wcf service. Since it wouldn't be very neat to make a datacontract client aware, I'm interested in the second solution. Although, how can I do a reset?

Or am I looking in the wrong direction? Should I cast an Dto to a client "domain/model" object?

Gr. Jeroen

Dec 15, 2010 at 9:38 AM
Edited Dec 15, 2010 at 9:41 AM

The ShoppingCartItem instead of *being* the datacontract, could just use it.

MVVM is a parttern used for decoupling logic and UI, so in this case you need to remember that the datacontract is probably your Model, the ShoppingCartItem could be the view-model and the WPF control the View. Following this logic, instead of using private members to store the VM properties (amount, item price etc.), you could use a proper back-end, just something like:

public class ShoppingCartItem : NotifyPropertyChangedBase
{
	private IShoppingCartItemData data;

	public long Id
	{
		   get { return data.Id; }
		   set
		  {
			  if (data.Id != value)
			  {
				   data.Id = value;
				   NotifyOfPropertyChange (() => Id);
			  }
		   }
	}			
	public decimal Width { get; set; }
	public decimal Height { get; set; }
	public int Amount
	{
		   get { return data.Amount; }
		   set
		  {
			  if (data.Amount != value)
			  {
				   data.Amount = value;
				   NotifyOfPropertyChange (() => Amount);
				   NotifyOfPropertyChange (() => TotalPrice);
			  }
		   }
	}
	public decimal ItemPrice
	{
		   get { return data.ItemPrice; }
		   set
		  {
			  if (data.ItemPrice!= value)
			  {
				   data.ItemPrice= value;
				   NotifyOfPropertyChange (() => ItemPrice);
				   NotifyOfPropertyChange (() => TotalPrice);
			  }
		   }
	}	
	public decimal TotalPrice
	{
		get { return data.Amount * data.ItemPrice; }
	}
}
Again, in this case the ShoppingCartItem is a view-modelm while the ShoppingCartItemData is a model.

If you really want to avoid this, you need to change this call in your code
        public void UpdateTotalprice(ShoppingCartItem shoppingCartItem)
        {
            //This goed terribly wrong!
            //orderedItems = new BindableCollection<ShoppingCartItem>(orderedItems);
            NotifyOfPropertyChange(() => OrderedItems);
        }
with this
        public void UpdateTotalprice(ShoppingCartItem shoppingCartItem)
        {
            OrderedItems.Refresh();
        }
but the performance of this solution will be awful (it forces a complete re-evaluation of the collection view) and this is not the best way to go. I would stick with a proper MVVM-ish implementation.

 

Dec 15, 2010 at 10:06 AM

Ah, I see! I should create an extra layer between my Dto's and the view (a VM offcourse). It isn't a problem that I'm not creating a sperate ShoppingCartItemView? 

Gr. Jeroen

Dec 15, 2010 at 10:20 AM
Edited Dec 15, 2010 at 10:21 AM

Well, you already have a ShoppingCartItem view... or better, you got many of them! :)

MVVM defines the role of the components (Model, View and ViewModel), bot not their implementation. This means that a view can be a control, a DataTemplate, or whatever fits the scenario.

In your specific case, I would consider the column datatemplates as multiple views of the same view-model (ShoppingCartItem); if you prefer identifying a view with a control, imagine to put the content of the column template into a user control and name them something like ShoppingCartItemAmountView, ShoppingCartItemIdView etc.

The fact is that such views will be only used inside the grid, so there is no need to share them outside the parent view where they are used.

 

So, short answer: no, it is not a problem, since MVVM does not require that views are shared controls. :)

Dec 15, 2010 at 10:29 AM

Hmm, this is an eye opener. Till now I always tried to make as much views as viewmodels, but as it seems, that's not necessary. Thank you very much for this information!