
Contents
Introduction
I guess most readers agree that a well designed application consists of some
logical layers or tiers. Common sense is to at least separate the user
interface from the underlying business objects and rules and the database
access layer. Usually, business objects (also referred to as business entities
- I use these terms here equally) are available in all layers and are the data
source for Windows forms. Binding these business objects to forms can be a
boring and error-prone task. With a new WinForm application coming up, I
decided to finally tackle this inconvenience. I found myself writing code like
this just too many times to tolerate this monotony and possible error source
any longer:
private void initialize()
{
txtName.Text = _person.Name;
dateOfBirth.Value = _person.DateOfBirth;
chkActive.Checked = _person.Active;
}
private void save()
{
_person.Name = txtName.Text;
_person.DateOfBirth = dateOfBirth.Value;
_person.Active = chkActive.Checked;
}
This might be a non-issue when there are only a few fields, everything is
specified and will not change in the course of a project, and the project
contains only a few forms. Facing, however, maybe up to hundred or more forms,
I wanted to bind these objects in the designer, assigning each input control a
property of the business entity with just a few clicks. A few months ago, I
covered a way to bind
XML documents - config files in particular - to WinForms, so I thought
it should be an easy task to extend support of binding to any object.
Design goals
The proposed solution is supposed to:
-
Simplify the binding of one business entity to a WinForm, in which this entity
is created/edited.
-
Provide a consistent method of handling the
IsDirty
state of the object (e.g., show an asterisk in the caption when the object is
dirty and remove it when it is not dirty).
-
Show up a consistent method of indicating the
IsValid
state of the
object (e.g., using the ErrorProvider
component).
How to read the code
To help you understand the code (especially the scope of methods and variables),
here are the relevant coding conventions I follow:
-
private fields start with a "_" prefix followed by lowercase.
-
private methods start with lowercase.
-
public or protected methods and properties (there are no non-private fields)
start with uppercase.
For clarity's sake, I omitted helper functions in this article where I thought
that the implementation is rather trivial and the method name tells the story
anyway (which a method name should always do ;-)). You can look them up in the
source code though.
The ObjectBindingManager
The ObjectBindingManager
is the key component of this package. It
abstracts the bound object and provides design-time capabilities for assigning
properties to controls. The idea behind is rather trivial. The component gets
dropped on the WinForm. It is then assigned a System.Type
to allow
the assignment via a PropertyExplorer
instead of typing a property
name and thus avoid errors. At runtime, the ObjectBindingManager
initializes
all bound controls with the corresponding properties. It further provides a CommitChanges()
and a RollbackChanges
method to handle the updated values. It also
listens to the Validated
event of all bound controls, compares the
new value with the old one, and raises an IsDirtyChanged
event to
notify a listener about the state of the object. By this, the form can listen
to this event and behave accordingly (e.g., the mentioned asterisk, but it
could also activate/deactivate the Save button).
The BoundType
is assigned by means of a custom UITypeEditor
called TypeBrowser
. I incidentally found this component when I
just wanted to write my own, so props here to Stephen Toub. I modified it
slightly to fit my needs. It now takes a System.Type
as parameter
and shows only types of this Type
or descendants. By this, you can
modify my code and provide the base type of your business objects. If you don't
have a base type, simply call it with a NULL
argument.
It then shows all types. The ObjectBindingManager
provides
extended properties, a very convenient technique to enhance other controls. All
you have to do is to tag the component with a ProvideProperty
attribute
and follow a simple naming convention:
private bool ShouldSerializeBinding(Control extendee)
{
Binding binding = _bindings[extendee];
if (binding == null) return false;
bool serialize = (binding.ControlProperty != null
&& binding.ControlProperty != "" &&
(binding.ObjectProperty != null &&
binding.ObjectProperty != "") || binding.MonitoredOnly);
return serialize;
}
[Category("Data")]
[Description("Define databinding on Object")]
public Binding GetBinding(Control extendee)
{
if (_bindings.ContainsKey(extendee))
return _bindings[extendee];
else
{
Binding binding = new Binding(ObjectType, extendee);
binding.ControlProperty = (extendee is CheckBox) ? "Checked" : "Text";
_bindings[extendee] = binding;
return binding;
}
}
public void SetBinding(Control extendee, object value)
{
Binding binding = value as Binding;
binding.Control = extendee;
if (!binding.MonitoredOnly &&
(binding.ControlProperty == null ||
binding.ObjectProperty == null))
_bindings.Remove(extendee);
else
if (_bindings.ContainsKey(extendee))
_bindings[extendee] = binding;
else
_bindings.Add(extendee, binding);
}
Here, the property binding will be provided to all controls. You can select a
control in the designer, switch to the property window, and you find an entry
"Binding on bindingManager1" under the DataBinding category. The ShouldSerializeBinding
method here tells the designer if code for this property should be generated.
Supporting complex types
Depending on the controls you use and your business framework, you might still
need to assign some values manually. In the sample application, you will find a
Flags
property which is displayed by two checkboxes. In case you
can't use the auto-binding feature but still want to track the IsDirty
-state,
you can "monitor" a control. Select the control, find the "Binding on
bindingManager1" entry, expand it, and set MonitoredOnly
to
true
. Now, the initial state of the control is compared against the
actual state when determining the IsDirty
state. After manual
initialization, call bindingManager.ResetMonitoredValues()
;.
public MainForm()
{
InitializeComponent();
Person person = Service.GetPerson();
bindingManager.BoundObject = person;
bindingManager.Bind();
chkUser.Checked = (person.Flags & GroupFlags.User)
== GroupFlags.User ? true : false;
chkAdmin.Checked = (person.Flags & GroupFlags.Admin)
== GroupFlags.Admin ? true : false;
bindingManager.ResetMonitoredValues();
}
How to use the code
Compile the code, add the ObjectBindingManager
to your toolbox, and
drop an instance on the form. Assign a BoundType
, select a control
to bind, find 'Binding on bindingManager' in the Data category of the property
window, and assign the ObjectProperty
.
Now, where do I go from here?
Because business object layers and needs differ greatly from each other, this is
most likely not a Plug&Play component. I assume you will modify the
argument to TypeBrowser
, so only your business objects are
displayed. Furthermore, you might enjoy the simplicity of handling the IsDirty
state and think of enhancing the BindingManager
to provide also
the IsValid
state - well, at least I did ... If you have a strong
and descriptive validation on your business objects (like, for example,
DataObjects.NET or CSLA.NET does), you might reduce greatly the need of user
interface validation code. The component has an ErrorProvider
property
- just drag one on the form, switch to the properties of the ObjectBindingManager
,
and assign it - which I use to display the validation errors.
You can switch the ObjectBindingManager
to a different mode by
disabling CacheValues
. Instead of caching the values in an object
array, the properties are directly bound using the DataBindings
collection
of the controls. I decided to enable it by default simply because I needed to.
It might suit your needs better by disabling it.
Finally, I can only recommend using a kind of EditorBaseForm
where
you put the ObjectBindingManager
, and handle its events and
provide a template Save
method ... it saved me a great deal of
time and headaches!
Revision Notes
- 27/09/2004: Fixed an issue concerning readonly properties and non-string
properties.