Print Page | Close Window

Calculated fields and NotifyPropertyChanged

Printed From: IdeaBlade
Category: DevForce
Forum Name: DevForce 2010
Forum Discription: For .NET 4.0
URL: http://www.ideablade.com/forum/forum_posts.asp?TID=2066
Printed Date: 10-Jun-2026 at 5:33pm


Topic: Calculated fields and NotifyPropertyChanged
Posted By: bigme
Subject: Calculated fields and NotifyPropertyChanged
Date Posted: 15-Aug-2010 at 4:55pm
I am new to DevForce and having trouble getting derived fields to work. In the WCF RIA world I'd simply use partial classes and virtual overrides to calculate things such as Tax and Totals. With DevForce I have the luxury of Interceptors, but I'm confused about how to raise a change notification in this scenario. I'm using MVVM, so lets start with the ViewModel:

Snippet
   public class MainViewModel : VM
    {
        private Order currentOrder;
        public Order CurrentOrder
        {
            get { return currentOrder; }
            set
            {
                if (currentOrder == value)
                    return;
                currentOrder = value;
                RaisePropertyChanged("CurrentOrder");
                WriteToLog(CurrentOrder == null ? "No Order" : "Found order number " + CurrentOrder.OrderID);
            }
        }

        private readonly NorthwindIBEntities mgr;
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            if (IsInDesignMode)
            {
                // Code runs in Blend --> create design time data.
            }
            else
            {
                mgr = new NorthwindIBEntities();
                  mgr.Fetching += (s, e) => WriteToLog(e.Query);
                mgr.Fetching += (s, e) => IsBusy = true;
                mgr.Queried += (s, e) => IsBusy = false;
                
                var q1 = from o in mgr.Orders
                         select o;
                q1.ExecuteAsync(op => {
                    CurrentOrder = op.Results.First();
                });
            }
        }

The CurrentOrder has a collection of OrderDetails, which is bound to a Grid like so:

Snippet
 <sdk:DataGrid AutoGenerateColumns="False" Height="200" ItemsSource="{Binding CurrentOrder.OrderDetails, Mode=TwoWay}">
        <sdk:DataGrid.Columns>
          <sdk:DataGridTextColumn Header="Quantity" Binding="{Binding Quantity, Mode=TwoWay}" />
          <sdk:DataGridTextColumn Header="Price" Binding="{Binding UnitPrice, Mode=TwoWay}" />
          <sdk:DataGridTextColumn Header="Total" Binding="{Binding Total, Mode=OneWay}" />
        </sdk:DataGrid.Columns>
Finally, because I need to calculate the Total, I've extended the generated OrderDetail class as follows:
Snippet
public partial class OrderDetail 
    {
        private decimal total;

        [Bindable(trueBindingDirection.TwoWay)]
        [Editable(false)]
        public Decimal Total
        {
            get { return total; }
            private set
            {
                if (total == value)
                    return;
                total = value;
                NotifyPropertyChanged("Total");
            }
        }

        [AfterSet(EntityPropertyNames.UnitPrice)]
        [AfterSet(EntityPropertyNames.Quantity)]
        [AfterSet(EntityPropertyNames.TaxID)]
        [AfterSet(EntityPropertyNames.Discount)]
        public void CalculateTotal(PropertyInterceptorArgs<OrderDetailString> args)
        {
            Decimal discountPrice = UnitPrice - UnitPrice *(Decimal)Discount;
            Decimal gst = Math.Round(discountPrice * Tax.Rate, 2);
            Total = Quantity * (discountPrice + gst) ;
        }

        // Declare the PropertyChanged event
        public event PropertyChangedEventHandler MyPropertyChanged;

        // NotifyPropertyChanged will raise the PropertyChanged event passing the
        // source property that is being updated.
        public void NotifyPropertyChanged(string propertyName)
        {
            if (MyPropertyChanged != null)
            {
                MyPropertyChanged(thisnew PropertyChangedEventArgs(propertyName));
            }
        }  

    }
There are several problems:

1. The total is not calculated. If I change the AfterSet to an AfterGet, then the Total IS calculated, but that seems very wrong to me.

2. Using an AfterGet, so that at least it triggers, if I set a breakpoint in the interceptor then I can see that the Total is calculated, but the value displayed is not updated.

I suspect that the problem is to do with change notification, but can't see what's wrong. Any help would be appreciated.

Dave.



Replies:
Posted By: ting
Date Posted: 17-Aug-2010 at 8:12pm
When anything Total depends on changes, you should recalculate Total and fire OnPropertyChanged() for "Total".  The DevForce Entity already implements INotifyPropertyChanged, so you shouldn't need your own event.  I've written what I think your code should look like below.  You could simplify and calculate Total on every get, but I cached the value in case you were concerned about performance.
 
 
  public partial class OrderDetail : IbEm.Entity {
    private decimal total;
    private bool totalInitialized = false;

    [Bindable(true, BindingDirection.TwoWay)]
    [Editable(false)]
    public Decimal Total {
      get {
        if (!totalInitialized) CalculateTotal();
        return total;
      }
    }

    [AfterSet(EntityPropertyNames.UnitPrice)]
    [AfterSet(EntityPropertyNames.Quantity)]
    [AfterSet(EntityPropertyNames.TaxID)]
    [AfterSet(EntityPropertyNames.Discount)]
    public void FireTotalChanged(PropertyInterceptorArgs<OrderDetail, String> args) {
      CalculateTotal();
      OnPropertyChanged(new PropertyChangedEventArgs("Total"));
    }

    public void CalculateTotal() {
      Decimal discountPrice = UnitPrice - UnitPrice * (Decimal)Discount;
      Decimal gst = Math.Round(discountPrice * Tax.Rate, 2);
      total = Quantity * (discountPrice + gst);
      totalInitialized = true;
    }
  }



Posted By: bigme
Date Posted: 17-Aug-2010 at 11:51pm
Thanks ting!

I don't know how I missed your implementation of OnPropertyChanged. And forgetting to calculate the total before getting was a pretty stupid error too! On the plus side, the quality of my forum posts can only improve :-)

Dave.



Posted By: bigme
Date Posted: 24-Aug-2010 at 11:29pm
I have a followup: how do I calculate a total from a collection of subtotals (eg the Order.TotalAmount from the sum of the OrderDetail.Totals).
Snippet
Snippet
    public partial class Order
    {

        [Bindable(trueBindingDirection.OneWay)]
        [Editable(false)]
        public Decimal Subtotal
        {
            get
            {
                decimal subtotal = OrderDetails.Sum(od => od.Total);
                return subtotal;
            }
        }
        
        [Bindable(trueBindingDirection.OneWay)]
        [Editable(false)]
        public Decimal TaxAmount
        {
            get
            {
                decimal taxAmount = OrderDetails.Sum(od => od.TotalTax);
                
                if (Tax != null)
                    taxAmount +=  (Freight.HasValue ? Freight.Value : 0.00M) * Tax.Rate;
               
                return taxAmount;
            }
        }
       
        [Bindable(trueBindingDirection.OneWay)]
        [Editable(false)]
        public Decimal TotalAmount
        {
            get
            {
                return Subtotal + (Freight.HasValue ? Freight.Value + Freight.Value * Tax.Rate : 0.00M) ;
            }
        }

        [AfterSet(EntityPropertyNames.OrderDetails)] // <= THIS NEVER FIRES ??
        [AfterSet(EntityPropertyNames.Freight)]
        [AfterSet(EntityPropertyNames.Tax)]
        private void FireTotalChanged(IPropertyInterceptorArgs args)
        {
            OnPropertyChanged(new PropertyChangedEventArgs("TotalAmount"));
        }

    }
The problem is that FireTotalChanged() is not called when any OrderDetails.Total changes.

Dave.



Posted By: ting
Date Posted: 25-Aug-2010 at 7:30pm
OrderDetails is a navigation property, so there is no setter which is why the AfterSet doesn't trigger.
 
Here's what I would do.
1)  On Order, define this method:
internal void OnTotalAmountChanged() {
  OnPropertyChanged(new PropertyChangedEventArgs("TotalAmount"));
}
 
2)  In the OrderDetail, whenever you know that Total changes, call this.Order.OnTotalAmountChanged().  Depending on your implementation, this might occur in multiple places.
 
3)  For refactoring purposes, have FireTotalChanged() call OnTotalAmountChanged().
 
Be very careful about propagating changed events across objects.  You can cause UI performance problems or infinite loops if you chain to many objects together.
 



Print Page | Close Window