4

I have a datagrid, an ObservableCollection of Product Types in a ViewModel and an implementation of EventToCommand like shown below. I would like to update the Total Column from the product of Quantity and Cost Column and save the changes without using the evil code behind or Windows Forms DataGridView. How can I achieve this? Datagrid:

<DataGrid x:Name="dataGrid" Margin="5,5,10,5" AutoGenerateColumns="False"  HorizontalAlignment="Stretch" ItemsSource="{Binding ProductList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Stretch" Height="566"  >
<i:Interaction.Triggers>
     <i:EventTrigger EventName="CellEditEnding" SourceObject="{Binding ElementName=Control}">
        <cmd:EventToCommand Command="{Binding EndEdit}" PassEventArgsToCommand="True"/>
     </i:EventTrigger>
</i:Interaction.Triggers>
<DataGrid.Columns>
  <DataGridTextColumn x:Name="Id" Binding="{Binding Path=Id, Mode=TwoWay}" Header="Id"/>
    <DataGridTextColumn x:Name="name" Binding="{Binding Path=Name, Mode=TwoWay}" Header="Name"/>
    <DataGridTextColumn x:Name="cost" Binding="{Binding Path=Cost, Mode=TwoWay}" Header="Cost"/>
    <DataGridTextColumn x:Name="Quantity" Binding="{Binding Path=Quantity, Mode=TwoWay}" Header="Quantity"/>
    <DataGridTextColumn x:Name="total" Binding="{Binding Path=Total, Mode=TwoWay}" Header="Total"/>
</DataGrid.Columns>

Then in the ViewModel

 private ObservableCollection<Product> _product;
    public ObservableCollection<Product> MyProduct
    {
        get
        {
            return _product;
        }
        set
        {
            Set(ref _product, value);
        }
    }

public ProductViewModel(IDataService proxy)
    {
        _proxy = proxy;

        LoadCommand = new RelayCommand(DoGetProducts);
        EndEdit = new RelayCommand<DataGridCellEditEndingEventArgs>(DoEndEdit);

    }

    private void DoEndEdit(DataGridCellEditEndingEventArgs obj)
    {
        DataGridRow row = obj.Row;
        Product p = (Product)row.Item;
        p.Total = p.Cost*p.Quantity;
        _proxy.SaveAll();
    }

Then in the Model:

public class DataService : IDataService
{
    ProductEntities context;
    public DataService()
    {
        context = new ProductEntities();
    }
    public ObservableCollection<Product> GetProducts(){
        ObservableCollection<Product> products = new ObservableCollection<Product>();
            foreach(var p in context.Products.Tolist()){
                products.add(p);
            }
        return products;
    }
    public void SaveAll()
    {
        context.SaveChanges();
    }
}

The datagrid is loading products but not updating the Total when Cost and Quantity is changed. Also, not saving the changes in database

KMarto
  • 300
  • 2
  • 23
  • If your models Total setter is implementing the INotifyPropertChanged interface (this is not shown) then the only suggestion I thing I can think of is adding UpdateSourceTrigger=PropertyChanged to you binding. – stuicidle Aug 16 '17 at 11:45
  • Why do you bind your `DataGrid` `ItemsSource` to a property named `ExamsList` when there is no such property in the ViewModel? Or do you forget this property in your example? – Sebastian Richter Aug 17 '17 at 09:26
  • @SebastianRichter that's a typo. However, it's not where the problem is coming from – KMarto Aug 18 '17 at 06:00
  • @stuicidle the model comes from an entity data model – KMarto Aug 18 '17 at 06:01
  • Your problem is solved finally. No need to code at all. See the solution – Ramankingdom Aug 19 '17 at 17:56

2 Answers2

2

For the "total" column in the DataGrid to get updated, the Product class should implement the INotifyPropertyChanged interface and raise the PropertyChanged event for the Total property:

private double _total;
public double Total
{
    get { return _total; }
    set { _total = value; OnPropertyChanged("Total"); }
}

And for you to be able to save the value to the database, you need to map the Total property against a column in your database table, just like you (hopefully) did with the other columns.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • First of all, I cannot bind to single property because it's a list of items. Also, in MVVM light, the `Set(ref, _property,value)` implements the INotifyPropertChanged. – KMarto Aug 13 '17 at 23:11
  • You are already binding to a singel property in your DataGridTextColumn...and it is the setter of the Total property of the Product class that should call the Set method, not the MyProduct property. – mm8 Aug 14 '17 at 08:45
  • This Item i am binding to comes from an observable collection of items like `ObservableCollection` so when i'm binding i bind to a property of an item from the collection like `Product.Price`. Do I need to create another single item and if so, how will data be loaded to the grid? I might be missing something from your answer. – KMarto Aug 18 '17 at 05:56
  • When you set the Total property of Product, the total column in your DataGrid will display this value only if the Product class implements the INotifyPropertyChanged interface and raise the PropertyChanged event in the setter of the Total property as I stated in my answer. Whether the ItemsSource property is bound to an ObservableCollection doesn't matter. – mm8 Aug 18 '17 at 09:02
0

The output will be like this:- enter image description here

Change your Xaml like give below

<DataGrid x:Name="dataGrid" Margin="5,5,10,5" AutoGenerateColumns="False"  HorizontalAlignment="Stretch" ItemsSource="{Binding ProductList}" VerticalAlignment="Stretch" Height="566"  >
<i:Interaction.Triggers>
     <i:EventTrigger EventName="RowEditEnding" ">
        <cmd:EventToCommand Command="{Binding EndEdit}" PassEventArgsToCommand="True"/>
     </i:EventTrigger>
</i:Interaction.Triggers>
<DataGrid.Columns>
  <DataGridTextColumn x:Name="Id" Binding="{Binding Path=Id, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Header="Id"/>
    <DataGridTextColumn x:Name="name" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Header="Name"/>
    <DataGridTextColumn x:Name="cost" Binding="{Binding Path=Cost, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Header="Cost"/>
    <DataGridTextColumn x:Name="Quantity" Binding="{Binding Path=Quantity, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Header="Quantity"/>
    <DataGridTextColumn x:Name="total" Binding="{Binding Path=Total, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" Header="Total"/>
</DataGrid.Columns>

And in View Model you Bindings be like

public BindingList<Product> ProductList
{
 get
  {
   return _proxy.ProductList;
  }
}

And EndEdit Command Should execute the following function

private void ExecuteEndEdit(DataGridRowEditEndingEventArgs param)
 {
  var product  = param.Row.Item as Product; 
  var result = ProductList.FirstOrDefault(p => p.Id == product.Id);
  var index= ProductList.IndexOf(result);
  result.Total = result.Cost * result.Quantity;
  ProductList.ResetItem(index);
 }

Your IDataService can Expose Binding List like

  public class DataService : IDataService
    {
        ProductEntities context;
        public DataService()
        {
            context = new ProductEntities();
        }
        public BindingList<Product> ProductList
        {
            get
            {
               //EDIT: YOU HAVE TO CALL context.Products.Load(); OR IT WILL RETURN EMPTY RESULTS
               context.Products.Load();
                return context.Products.Local.ToBindingList<Product>();
            }
        }
        public void SaveAll()
        {
            context.SaveChanges();
        }
    }

The Context.Save will save your code.

KMarto
  • 300
  • 2
  • 23
Ramankingdom
  • 1,478
  • 2
  • 12
  • 17
  • Please refactor as per your need. :) – Ramankingdom Aug 19 '17 at 18:05
  • can anybody explain why. its marked not as answer without testing. this is working solution – Ramankingdom Aug 20 '17 at 07:06
  • Let me test the code I will mark it as answer soon ignore the downvoters – KMarto Aug 22 '17 at 05:53
  • how do you initialize the ProductList because `ProductList = new new BindingList();` and `ProductList =context.Products.Local.ToBindingList();` doesn't seem to work while `foreach (var p in context.Products) { ProductsList.Add(p); }` seems to add each item twice and does not save on `context.SaveChanges()` – KMarto Aug 22 '17 at 07:14
  • no I don't initialize it. You just directly take context.Products.Local.ToBindingList(); no need to create it in IdataService. Just Expose the real collection from data service. It will ensure consistency – Ramankingdom Aug 22 '17 at 07:17
  • you can expose via dataservice – Ramankingdom Aug 22 '17 at 07:18
  • Updating answer as per your convenience – Ramankingdom Aug 22 '17 at 07:23
  • I Get that. However my binding datacontext for the view is the viewmodel and the Property ProductList and EventToCommand is defined in the VM. So Now I have a Method prototype in IDataService Called `BindingListGetProducts()` and implemented in DataService as `public BindingList GetProducts(){ return context.Products.Local.ToBindingList(); }` – KMarto Aug 22 '17 at 07:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/152498/discussion-between-martin-kariuki-and-ramankingdom). – KMarto Aug 22 '17 at 07:37