http://rs6.net/tn.jsp?t=bup6w7bab.0.lirg6ubab.zufnlmbab.2355&ts=S0253&p=http%3A%2F%2Fwww.ideablade.com%2Ftech_tips_definitions.htm - Level 200 DevForce Express |
http://rs6.net/tn.jsp?t=bup6w7bab.0.nblx5vbab.zufnlmbab.2355&ts=S0253&p=http%3A%2F%2Fwww.ideablade.com%2Ftechtips_summary.html - View Archived Tech Tips
|
http://rs6.net/tn.jsp?t=bup6w7bab.0.nblx5vbab.zufnlmbab.2355&ts=S0253&p=http%3A%2F%2Fwww.ideablade.com%2Ftechtips_summary.html"> |
Refreshing Dependent Parents |
|
Suppose you have business objects with computed properties that depend upon values in child objects. How do you get a display of the Parent to refresh when the values in the children change?
To make it all quite concrete, let’s consider a specific use case: you have Customers who have Orders and who have made payments that are applied against those Orders. A given Payment might entirely cover one or more orders, then only partially cover another; so you also have PaymentAllocation objects that permit you to make multiple discrete allocations of the funds from a given payment to a number of Orders. |
|
|
|
In the screen shot above, the $49.13 allocation from Payment 1 to Order 10692 combines with allocations from one or more other payments to satisfy the outstanding balance on that Order. But suppose we reduce that allocation to $40. That should cause cells in both the Payments and Orders grid to change. In the Payment grid, the Total Allocs and Unallocated columns should change. In the Orders grid, the Total Payments and Balance Due columns should change for the targetted Order. But it doesn’t happen: |
|
|
|
Why not?
You may know that DevForce business objects have a PropertyChanged event. DevForce ensures that this event fires for persistable properties – those that map to columns in a back-end data source, and therefore to columns in a DataTable in the DevForce cache. It also generates the setters for one-to-one relation properties (like an Order’s Customer) in such a way that they also raise the PropertyChanged event when their value is changed. If the PropertyChanged event is raised for an object, any data bindings involving the changed property will hear about it, and the data binding will inform the affected UI control that a change has occurred. In most cases – we’ll discuss the exception of the .NET DataGridView control below – the control will respond by repainting itself so that it displays the new value.
Computed properties don’t cause the PropertyChanged event to fire, however. Their values are neither held in a DataTable nor changed directly, but are instead simply baked and delivered on demand. This can create a problem in the UI when persistable properties upon which a computed property depends are changed, because, the moment those independent values change, the displayed value of the computed property will no longer be current. Does its display get refreshed?
The answer depends upon whether the independent property that got changed lives in the same object, or an external one. Suppose, for example, that you have an Employee class that defines a (computed) Age property whose values depends upon the Employee’s (persistable) BirthDate (as illustrated in many of our Fundamentals tutorials). If a user changes the BirthDate value, PropertyChanged will fire on the Employee instance. Data bindings to that Employee in a form or UserControl listen for this event, and they will notify their controls when it occurs. All the data bindings for the Employee instance will get updated, so the Age value will be updated to correspond to the new BirthDate.
But what if the independent values on which a given computed property depends live in a different object? When one of those independent value changes, PropertyChanged will fire on its object, but not on the object that includes the computed property. The data bindings for the latter object won’t near about the change, and the computed property will continue to display an obsolete value.
In this situation the objects that do change must tell the objects that depend upon them to raise their PropertyChanged events, too. For example, you know that changing the Order to which a given Payment Allocation is to be applied is going to affect some computed values in two different Orders. Money is going to be disallocated from one Order and applied against a different one. You know that changing the Payment from which a given Payment Allocation is drawn will change computed values in the related Payments: allocatable funds are going to be restored to one Payment and taken away from another. Finally, you know that changing the Amount of an allocation is going to affect computed property values in both the related Order and the related Payment.
So you must teach the PaymentAllocation object, when its state changes in certain ways, to send a message to those related Order and Payment objects that their state has also changed, and that they should so inform their consumers. In DevForce Entity (business) classes, we’ve provided a convenient mechanism for this purpose.
The ForcePropertyChanged () Method
Every DevForce-generated business class includes a method named ForcePropertyChanged() that can be called on an instance of that class to raise the instance’s PropertyChanged event. We’ll use this at the appropriate time to tell dependent parents to announce to the world that they’ve changed. That way, other application mechanisms can respond as required. Data bindings, in particular, will make a fresh request for property values, and so will get updated values for calculated ones.
Let’s enhance our PaymentAllocation class to perform the necessary notifications. We’ll override the definitions for the Order, Payment, and Amount properties, leaving their existing functionality intact, but also inserting in their setters a call to ForcePropertyChanged() on the appropriate dependent parent: |
| |
C#:
public override Order Order { get { return base.Order; } set { base.Order = value; //Order has computed properties that roll up these payment allocations. this.Order.ForcePropertyChanged(null); } }
public override Payment Payment { get { return base.Payment; } set { base.Payment = value; //Payment has computed properties that roll up these payment allocations. this.Payment.ForcePropertyChanged(null); } }
public override decimal Amount { get { return base.Amount; } set { base.Amount = value; //Order has computed properties that roll up these payment allocations. //Payment has computed properties that roll up these payment allocations. this.Order.ForcePropertyChanged(null); this.Payment.ForcePropertyChanged(null); } }
VB.NET:
Public Overrides Property Order() As Order Get Return MyBase.Order End Get Set(ByVal value As Order) MyBase.Order = value 'Order has computed properties that roll up these payment allocations. Me.Order.ForcePropertyChanged(Nothing) End Set End Property
Public Overrides Property Payment() As Payment Get Return MyBase.Payment End Get Set(ByVal value As Payment) MyBase.Payment = value 'Payment has computed properties that roll up these payment allocations. Me.Payment.ForcePropertyChanged(Nothing) End Set End Property
Public Overrides Property Amount() As Decimal Get Return MyBase.Amount End Get Set(ByVal value As Decimal) MyBase.Amount = value 'Order has computed properties that roll up these payment allocations. 'Payment has computed properties that roll up these payment allocations. Me.Order.ForcePropertyChanged(Nothing) Me.Payment.ForcePropertyChanged(Nothing) End Set End Property |
|
In any of the screen shots, you can see that the Customer object, like the Order and Payment objects, also includes some child-dependent computed properties. The Total Price Of Orders, Total Payments, and Balance Due depend upon values in the customers’ Orders and Payments. We’ll have the same issue there, so let’s propagate the effect of changes in the latter objects up their dependency chains, too. We’ll so so by overriding appropriate properties in the Payment and Order classes and calling ForcePropertyChanged() in their setters, just as we did for the selected PaymentAllocation properties: |
| |
C#:
Overrides in the Payment class:
public override decimal Amount { get { return base.Amount; } set { base.Amount = value; this.Customer.ForcePropertyChanged(null); } }
public override Customer Customer { get { return base.Customer; } set { base.Customer = value; this.Customer.ForcePropertyChanged(null); } }
Overrides in the Order class:
public override System.Nullable<decimal> FreightCost { get { return base.FreightCost; } set { base.FreightCost = value; //Customer's TotalPriceOfOrders is a function of the price //of its individual orders, whose OrderPrice each depends upon //the FreightCost. this.Customer.ForcePropertyChanged(null); } }
|
| |
VB.NET:
Overrides in the Payment class:
Public Overrides Property Amount() As Decimal Get Return MyBase.Amount End Get Set(ByVal value As Decimal) MyBase.Amount = value Me.Customer.ForcePropertyChanged(Nothing) End Set End Property
Overrides in the Order class:
Public Overrides Property () As System.Nullable(Of Decimal) Get Return MyBase.FreightCost End Get Set(ByVal value As System.Nullable(Of Decimal)) MyBase.FreightCost = value Customer's TotalPriceOfOrders is a function of the price 'of its individual orders, whose OrderPrice each depends upon 'the FreightCost. Me.Customer.ForcePropertyChanged(Nothing) End Set End Property
|
|
Testing Our Solution
Now that we have all the necessary calls to ForcePropertyChanged() in place, let’s test our app. In theory, since DevForce business objects implement .NET’s INotifyPropertyChanged interface and raise the PropertyChanged event, our data bindings should be bi-directional and the display controls on our form should refresh automatically. Let’s start by upping the Amount of the Payment 1 from $350 to $400:
|
|
The customer’s Total Payments went from $550 to $600, and their Balance Due dropped from $98.37 to $48.37. So the dependency linkage between Payment and Customer is working great! On the other hand, the amount in the UnAllocated cell in the Payment row didn’t change, and it should have: after all, the relationship between Amount and Unallocated in Payment is just like that between BirthDate and Age in the Employee we discussed just a moment ago.
Something’s not working properly there, but before we get into it, let’s try changing an Allocation amount. We’ll reduce the $11.80 allocation to order 10692 to $5.00:
|
|
|
|
Again the display isn’t getting updated as it should. The Total Allocs and UnAllocated cells for Payment 2 should have changed, as should the Total Paymts and Balance Due cells for Order 10692. But they didn’t. What gives?
A Repaint Bug in the .NET DataGridView
The Payments and Orders grids aren’t showing any changes, even though we’ve already demonstrated that raising the appropriate PropertyChanged event is sufficient to cause an update to the display. As it so happens, there is simply a bug in the .NET DataGridView that manifests as a failure to repaint when it should. If you have an app with similar data linkages, you can demonstrate for yourself that the problem is just one of repainting by simply minimizing and restoring the form (or by resizing the form, if the grids are anchored so that they resize, too). Neither of those actions causes any data to be reloaded, but both force the screen to repaint. The correct values magically appear. (Remember, we also took $9.13 of allocation from Payment 1 away from Order 10692.)
|
|
 |
|
You can also demonstrate an inconsistency in the DataGridView’s refresh behavior. Suppose you change the Amount value again. Then, instead of clicking away from the Amount cell, press TAB or SHIFT+TAB to move left or right out of the cell. These particular navigational actions, it seems, do cause the DataGridView to refresh itself!
These refresh problems are specific to the .NET DataGridView. They don’t occur with the Developer Express XtraGrid or the Infragistics UltraGrid -- we tested them – and, as you’ve seen, they don’t occur with the .NET Winform loose controls, either.
So, we have some options. We can’t very well ask our end user to resize her screen after every change in order to see the right data. And, for any number of reasons, it may not be an option to switch to a different grid control.
So let’s stick with the .NET DataGridView, and see if we can find a workaround. We’ll have to force the repaint explicitly. To do so at the proper time, we need some event that will fire whenever a displayed PaymentAllocation is modified. We’ll use the CurrentItemChanged event on the BindingSource for the PaymentAllocations grid.
As it so happens, the form we’ve used for the screen shots in this article is built up by composition from a number of UserControls. A MainForm loads a CustomerUserControl; the CustomerUserControl loads an OrdersUserControl and a PaymentsUserControl; and the PaymentsUserControl loads a PaymentAllocationsUserControl. Since we have all these containers within containers, we have to decide where we want to put the event handler for the PaymentAllocations BindingSource.
We’ll put it in the CustomerUserControl, since that’s the control lowest in our hierarchy that contains both the Orders and Payments grids, which are the things that need to be explicitly refreshed. The BindingSource for payment allocations, named mPaymentAllocationsBS, is declared in the PaymentAllocationsUserControl, and scoped as Internal [C#] / Friend [VB], so it’s visible to its containers. We’ll reach down from the CustomerUserControl through the PaymentsUserControl and into the PaymentAllocationsUserControl to set up our event handler for CurrentItemChanged. Here’s the code, located in the code behind for the CustomerUserControl:
|
| |
C#:
//The following handler compensates for a repaint defect of the .NET DataGridView. //See comments in handler header. private BindingSource allocationsBS = mPaymentsUserControl.mPaymentAllocationsUserControl.mPaymentAllocationsBS; allocationsBS.CurrentItemChanged += new System.EventHandler(CurrentAllocationChanged);
/// <summary> /// The current PaymentAllocation item changed, potentially causing property value /// changes in its parent Payment and/or Order. A handler for the PaymentAllocation's /// PropertyChangedEvent forces the same event to fire on those parents, causing the /// data binding to inform the grids in which they are displayed that their display is /// invalid and should be repainted. /// Unfortunately, due to an infelicity in the .NET DataGridView, the grids *don't* /// repaint themselves, so we call Refresh() here to repaint everything /// in the display region of this UserControl. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> /// <remarks></remarks> Private Sub CurrentAllocationChanged(ByVal sender As object, ByVal e As System.EventArgs); this.mOrdersUserControl.mOrdersDGV.Refresh(); this.mPaymentsUserControl.mPaymentsDGV.Refresh(); }
VB.NET:
'The following handler compensates for a repaint defect of the .NET DataGridView. 'See comments in handler header. Dim allocationsBS As BindingSource = _ mPaymentsUserControl.mPaymentAllocationsUserControl.mPaymentAllocationsBS AddHandler allocationsBS.CurrentItemChanged, AddressOf CurrentAllocationChanged
''' <summary> ''' The current PaymentAllocation item changed, potentially causing property value ''' changes in its parent Payment and/or Order. A handler for the PaymentAllocation's ''' PropertyChangedEvent forces the same event to fire on those parents, causing the ''' data binding to inform the grids in which they are displayed that their display is ''' invalid and should be repainted. Unfortunately, due to an infelicity in the .NET ''' DataGridView, the grids *don't* repaint themselves, so we call Refresh() here to ''' repaint everything in the display region of this UserControl. ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Private Sub CurrentAllocationChanged( _ ByVal sender As Object, ByVal e As System.EventArgs) Me.mOrdersUserControl.mOrdersDGV.Refresh() Me.mPaymentsUserControl.mPaymentsDGV.Refresh() End Sub |
|
That takes care of the repainting problem, and our form works entirely as desired. If the repainting infelicity gets fixed at some future date, or we use a different grid control that doesn’t share the problem, we can simply remove the above code and let the other components do their jobs.
Conclusion
As often happens in the real world, we had to get into some dirty details, and deal with some not-quite-optimal behaviors in components over which we have no control, to get our app to work as we wanted it to. But the basics were straightforward, with our DevForce business objects. If an object has computed properties that depend on external objects, it’s going to depend upon those external objects to let it know when it should announce that it has been updated. We used the ForcePropertyChanged() method, called from property setters, to perform that notification.
If the bound controls do their part, that’s all you have to do! | |
|