| |
DevForce® Classic Tech Tips |
|
|
|
| Refresheshing Dependent Parents |  |
| Refreshing Dependent
Parents |
Level 200
DevForce Express |
May 21,
2007 |
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! |
|
|
|
|
|