Vishal
Vishal

Reputation: 6368

Property bound to DataGrid's SelectedItem does not change its child properties when CellEditEnding is fired

I have a DataGrid which looks like:

<DataGrid Grid.Row="3" Grid.Column="1" ItemsSource="{Binding Purchases}" SelectionMode="Single" SelectionUnit="FullRow"
          SelectedItem="{Binding SelectedPurchase, Source={x:Static ex:ServiceLocator.Instance}}" 
          AutoGenerateColumns="False" CanUserAddRows="False">

    <e:Interaction.Triggers>
        <e:EventTrigger EventName="CellEditEnding">
            <e:InvokeCommandAction Command="{Binding DataContext.CellEditEndingCommand, 
                                                     RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Page}}}"/>
        </e:EventTrigger>
    </e:Interaction.Triggers>

    <DataGrid.Columns>
        .......
        ........
    <DataGrid.Columns>

</DataGrid>

Property SelectedPurchase looks like:

private Purchase _selectedPurchase;
public Purchase SelectedPurchase
{
    get
    {
        return _selectedPurchase;
    }
    set
    {
        _selectedPurchase = value;
        NotifyPropertyChanged("SelectedPurchase");
    }
}

CellEditEndingCommand

public ICommand CellEditEndingCommand { get; set; }
private void CellEditEndingMethod(object obj)
{
    XDocument xmlPurchases = XDocument.Load(DirectoryPaths.DataDirectory + "Purchases.xml");
    var currentPurchaseInData = (from purchase in xmlPurchases.Element("Purchases").Elements("Purchase")
                                 where Convert.ToInt32(purchase.Attribute("Id").Value) == ServiceLocator.Instance.SelectedPurchase.Id
                                 select purchase).FirstOrDefault();

    currentPurchaseInData.SetElementValue("CreditorId", ServiceLocator.Instance.SelectedPurchase.Creditor.Id);
    currentPurchaseInData.SetElementValue("AnimalId", ServiceLocator.Instance.SelectedPurchase.Animal.Id);
    currentPurchaseInData.SetElementValue("QuantityInLitre", ServiceLocator.Instance.SelectedPurchase.Litre);
    currentPurchaseInData.SetElementValue("FAT", ServiceLocator.Instance.SelectedPurchase.FAT);
    currentPurchaseInData.SetElementValue("RatePerLitre", ServiceLocator.Instance.SelectedPurchase.RatePerLitre);

    xmlPurchases.Save(DirectoryPaths.DataDirectory + "Purchases.xml");
}

Now If I change any value in DataGridCell and then I hit Enter CellEditEndingCommand is fired and CellEditEndingMethod is fired. But If I keep a breakpoint inside CellEditEndingMethod and take a look at it, then I can see that Values of any property of SelectedPurchase does not change to new values.

Let me give an example to explain the above line more correctly:

When I keep a breakpoint on any line inside CellEditEndingMethod and take a look at Properties like Litre, FAT etc., these properties values does not change. I mean I expect the property to take new value but it holds old value. Also, In view I can see the new values but in XML file there are still old values.

Update:

Purchases = new ObservableCollection<Purchase>(
    from purchase in XDocument.Load(DirectoryPaths.DataDirectory + "Purchases.xml")
                              .Element("Purchases").Elements("Purchase")
    select new Purchase
    {
        Id = Convert.ToInt32(purchase.Attribute("Id").Value),
        Creditor = (
                        from creditor in XDocument.Load(DirectoryPaths.DataDirectory + "Creditors.xml")
                                                  .Element("Creditors").Elements("Creditor")
                        where creditor.Attribute("Id").Value == purchase.Element("CreditorId").Value
                        select new Creditor
                        {
                            Id = Convert.ToInt32(creditor.Attribute("Id").Value),
                            NameInEnglish = creditor.Element("NameInEnglish").Value,
                            NameInGujarati = creditor.Element("NameInGujarati").Value,
                            Gender = (
                                        from gender in XDocument.Load(DirectoryPaths.DataDirectory + @"Basic\Genders.xml")
                                                                .Element("Genders").Elements("Gender")
                                        where gender.Attribute("Id").Value == creditor.Element("GenderId").Value
                                        select new Gender
                                        {
                                            Id = Convert.ToInt32(gender.Attribute("Id").Value),
                                            Type = gender.Element("Type").Value,
                                            ImageData = gender.Element("ImageData").Value
                                        }
                                     ).FirstOrDefault(),
                            IsRegisteredMember = creditor.Element("IsRegisteredMember").Value == "Yes" ? true : false,
                            Address = creditor.Element("Address").Value,
                            City = creditor.Element("City").Value,
                            ContactNo1 = creditor.Element("ContactNo1").Value,
                            ContactNo2 = creditor.Element("ContactNo2").Value
                        }
                   ).FirstOrDefault(),
        Animal = (
                    from animal in XDocument.Load(DirectoryPaths.DataDirectory + @"Basic\Animals.xml")
                                            .Element("Animals").Elements("Animal")
                    where animal.Attribute("Id").Value == purchase.Element("AnimalId").Value
                    select new Animal
                    {
                        Id = Convert.ToInt32(animal.Attribute("Id").Value),
                        Type = animal.Element("Type").Value,
                        ImageData = animal.Element("ImageData").Value,
                        Colour = animal.Element("Colour").Value
                    }
                 ).FirstOrDefault(),
        Litre = Convert.ToDouble(purchase.Element("QuantityInLitre").Value),
        FAT = Convert.ToDouble(purchase.Element("FAT").Value),
        RatePerLitre = Convert.ToDouble(purchase.Element("RatePerLitre").Value)
    }
  );

Upvotes: 1

Views: 1390

Answers (3)

Fratyx
Fratyx

Reputation: 5797

The CellEditEnding Event is not meant to update the datarow but to validate the single cell and keep it in editing mode if the content is not valid. The real update is done when the whole row is committed. Try it by adding the code in the HandleMainDataGridCellEditEnding method in http://codefluff.blogspot.de/2010/05/commiting-bound-cell-changes.html to your CellEditEndingMethod. It is good explained there. You may replace the if (!isManualEditCommit) {} by if (isManualEditCommit) return;.

UPDATE

You can extend your Purchase class by interface IEditableObject. DataGrid will call the method EndEdit() of this interface after the data has been committed and so you can do the XML stuff there. So you don't need any further buttons because a cell goes in edit mode automatically and the commit is done when you leave the row. I think the CollectionChanged solution does not work because if you edit a dataset all changes take place inside the single object (Purchase) and not in the collection. CollectionChanged will be called by adding or removing an object to the collection

2nd UPDATE

Another try by putting it all together:

I simplified your Purchase class for demonstration:

class Purchase
{
    public string FieldA { get; set; }
    public string FieldB { get; set; }
}

Create a derived class to keep the real Purchase class clean:

class EditablePurchase : Purchase, IEditableObject
{
    public Action<Purchase> Edited { get; set; }

    private int numEdits;
    public void BeginEdit()
    {
        numEdits++;
    }

    public void CancelEdit()
    {
        numEdits--;
    }

    public void EndEdit()
    {
        if (--numEdits == 0)
        {
            if (Edited != null)
                Edited(this);
        }
    }
}

This is explained in SO WPF DataGrid calls BeginEdit on an IEditableObject two times?

And create the Purchases collection:

   ObservableCollection<EditablePurchase> Purchases = new ObservableCollection<EditablePurchase>()
        {
            new EditablePurchase {FieldA = "Field_A_1", FieldB = "Field_B_1", Edited = UpdateAction},
            new EditablePurchase {FieldA = "Field_A_2", FieldB = "Field_B_2", Edited = UpdateAction}
        };

    Purchases.CollectionChanged += Purchases_CollectionChanged;

    private void Purchases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
            foreach (EditablePurchase item in e.NewItems)
                item.Edited = UpdateAction;
    }

    void UpdateAction(Purchase purchase)
    {
        // Save XML
    }

This provides that the calls to Edited are catched for all EditablePurchase elements from initialization and for newly created ones. Be sure to have the Edited property set in initializer

Upvotes: 1

Fratyx
Fratyx

Reputation: 5797

You will not get any CollectionChanged event before DataGrid is changing the collection. And this does not before happen a dataset is committed. If you press 'Enter' in a cell you change the value of this cell in a kind of copy of the real dataset. So it is possible to skip the changes by rollback. Only after finishing a row e.g. by changing to another row or direct commit your changed data will be written back to the original data. THEN the bindings will be updated and the collection is changed. If you want to have an update cell by cell you have to force the commit as in the code I suggested. But if you want to have a puristic MVVM solution without code behind you have to be content with the behavior DataGrid is intended for. And that is to update after row is finished.

Upvotes: 0

Mike Fuchs
Mike Fuchs

Reputation: 12319

This is a disgrace for WPF. No DataGrid.CellEditEnded event? Ridiculous, and I didn't know about that so far. It's an interesting question.

As Fratyx mentioned, you can call

dataGrid.CommitEdit(DataGridEditingUnit.Row, true);

in a code behind CellEditEnding method. While it works, I find it's quite ugly. Not only because of having code behind (could use a behavior to circumnavigate that), but your ViewModel CellEditEndingMethod will be called twice, once for no good reason because the edit is not yet committed.

I would probably opt to implement INotifyPropertyChanged in your Purchase class (I recommend using a base class so you can write properties on one line again) if you haven't already, and use the PropertyChanged event instead:

public MyViewModel()
{
    Purchases = new ObservableCollection<Purchase>();
    Purchases.CollectionChanged += Purchases_CollectionChanged;
}

private void Purchases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems != null)
        foreach (Purchase item in e.NewItems)
            item.PropertyChanged += Purchase_PropertyChanged;

    if (e.OldItems != null)
        foreach (Purchase item in e.OldItems)
            item.PropertyChanged -= Purchase_PropertyChanged;
}

private void Purchase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // save the xml...
}

Upvotes: 0

Related Questions