New Posts New Posts RSS Feed: Calculated fields and NotifyPropertyChanged
  FAQ FAQ  Forum Search   Calendar   Register Register  Login Login

Calculated fields and NotifyPropertyChanged

 Post Reply Post Reply
Author
bigme View Drop Down
Newbie
Newbie
Avatar

Joined: 13-Aug-2010
Location: Australia
Posts: 14
Post Options Post Options   Quote bigme Quote  Post ReplyReply Direct Link To This Post Topic: Calculated fields and NotifyPropertyChanged
    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.
Back to Top
ting View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 27-Mar-2009
Location: San Francisco
Posts: 426
Post Options Post Options   Quote ting Quote  Post ReplyReply Direct Link To This Post 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;
    }
  }



Edited by ting - 17-Aug-2010 at 8:14pm
Back to Top
bigme View Drop Down
Newbie
Newbie
Avatar

Joined: 13-Aug-2010
Location: Australia
Posts: 14
Post Options Post Options   Quote bigme Quote  Post ReplyReply Direct Link To This Post 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.

Back to Top
bigme View Drop Down
Newbie
Newbie
Avatar

Joined: 13-Aug-2010
Location: Australia
Posts: 14
Post Options Post Options   Quote bigme Quote  Post ReplyReply Direct Link To This Post 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.

Back to Top
ting View Drop Down
IdeaBlade
IdeaBlade
Avatar

Joined: 27-Mar-2009
Location: San Francisco
Posts: 426
Post Options Post Options   Quote ting Quote  Post ReplyReply Direct Link To This Post 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.
 
Back to Top
 Post Reply Post Reply

Forum Jump Forum Permissions View Drop Down