WPF Two-way Databinding in ASP.NET - Enabling MVVM
INTRODUCTION
From the very outset, two-way databinding support in ASP.NET Web Forms has been poor. Over time, a number of solutions have evolved (keep reading to find out about some of these). Unfortunately, all of them have had significant limitations or have only worked when utilised in a proscribed manner.
Anyone moving from a Silverlight or WPF project to Web Forms will find themselves having to re-adjust their thinking somewhat from a stateful world to a stateless one. This transition forces a number of concessions, one of the most glaring of which is this absence of a rich and flexible two-way databinding model as supplied by the former frameworks. The powerful data binding support supplied by WPF also compliments using the MVVM pattern to such a degree that the combination of Databinding + MVVM has become the de facto pattern used to develop UI applications with WPF and Silverlight. Many people will agree that the inability to use this pattern when designing Web Forms pages feels like a real step backwards after spending anytime with WPF or Silverlight.
The aim of the proposed framework is to address the lack of flexible and powerful two-way data binding in ASP.NET Web Forms by allowing for a WPF-esque declarative syntax to be used which at the same time allows UI development using the MVVM pattern.
Please be aware that throughout this article the term "bind" is used to describe the action of displaying data from ViewModel/source on the screen. The term "unbind" is used to describe the reverse of this process: extracting the user input data from the control and mapping it back to the ViewModel.
BACKGROUND
References/Concepts
Some of the concepts I have assumed you are familiar with:
- WPF Data Binding
- MVVM
- More MVVM
- Mastering ASP.NET DataBinding
- INotifyPropertyChanged
- ICommand
- DelegateCommand
Current Solutions
Solution | Main Drawback |
---|---|
Subclass all controls | You have to subclass every control which needs to support two-way binding! |
Data Source controls and GridView, FormView, DetailsView | You're limited to using the listed controls. Factory methods/CRUD methods and parameter mappings required. |
Use Visual Studio Designer to create bindings at design time | No runtime support. You must use the Visual Studio designers. |
Extender Controls for each binding | Each binding requires a binding |
Parsing ASP.NET source files at runtime | Limited when binding across MasterPages/ContentPages and UserControls as you are reading the source from the file system. |
Binding Manager | No inline (ASPX) declarative binding. |
By hand | Labour intensive, verbose, accident prone, poor maintainability, code bloat. |
N.B. The above list of solutions for providing Web Forms with two-way data binding support is by no means exhaustive, but I do feel that it covers some of the more common methods used.
You may decide you prefer one of the above options to the proposed framework. I've listed them for this very reason, different scenarios call for different solutions and it pays to be aware of what's around. All of the above will work fine and will fit into various architectural designs, but I feel that the proposed framework offers some benefits. I hope that by explaining the approach I have taken with this framework, I will convince you of the same. If you feel that a deeper examination of the available two-way databinding methods would be of benefit, then please leave a comment and I will consider expanding on the merits and drawbacks of the methods listed above, but I do feel confident that if you've spent time doing databinding the WPF way, then you'll immediately understand how the approach I've taken can be of benefit.
DESIGN TENANTS
The following are a list of ideals which I have tried to adhere to whilst developing this framework:
- No page base class - In order to allow easy
integration of this framework with existing frameworks, a number of which require you to inherit your Pages from a base class, it was decided that we would not require this. - Minimise wire-up code - A key goal was to keep the amount of wiring required to a minimum. Taken hand-in-hand with the "no page base class" goal, this required careful design and implementation.
- Minimise code-behind - Eliminate, as much as possible, the need for any code-behind. The ability to implement the UI entirely declaratively was a key goal.
- Facilitate MVVM in ASP.NET - Full support for command binding and two-way databinding.
- Mimic WPF - Allow the use of WPF binding features such as
IValueConverter
s, Binding Modes, Resources, and expressive, declarative binding statements. - Suppress Exceptions - As with WPF, databinding errors should not cause your application to throw an Exception (partialy realised, see: "How far along are we?").
FEATURES THAT MADE IT...
- One-way and two-way data binding
- Tiny amounts of integration code
IValueConverter
support- Implicit and explicit precedence - If you bind multiple controls to a single source, you can control which control "Wins"/"IsAuthorative" on unbind
- Global binding resources - Allow one binding declaration to be used across multiple controls
- Stateful and stateless binding - Choose to persist the ViewModel in the View State between postbacks, or recreate it each time
- Cascading updates (see section with same title for more information)
- Automatic or explicit unbind - Choose to have the framework automatically unbind on each post back, or manually initiate the unbind operation when required
- Fully
unit testable - Dependency Injection/Inversion of Control utilised to allow easy mocking - Support deep binding paths - Will happily traverse and bind to child properties (no limit to the size of the object graphs)
- Full support for declarative binding
- Partial support for programmatic binding
- No base classes required - Allows easy integration with existing frameworks
- Full support for static or dynamic binding - Use predefined data bindings or generate them based on application flow/data flow
- Relative or absolute binding paths - Bind "out of context" using absolute binding expressions or as part of a nested hierarchy using relative binding paths
- Works entirely in nested scenarios - Data contexts are inherited from the page or parent binding
...AND SOME THAT DIDN'T
- Property coercion
- Built-in validation
- Cascading updates without
INotifyPropertyChanged
- possible as we know when values have changed - UI Element binding - The ability to bind to the properties of other controls (ala WPF)
- Ancestor binding - The ability to bind to Ancestors of a certain type
- Custom View State serializer - avoid the need for
[field: NonSerialized]
when usingINotifyPropertyChanged
- Support for
IDataErrorInfo
- The ability to have multiple models/contexts - Utilise multiple View Models/data contexts via a dictionary based view data context collection
USING THE CODE
Solution Overview
Project | Description |
---|---|
Framework/Binding | This is the core framework assembly. It contains all the essential non-platform specific framework components. |
Framework/ASPBinding | ASP.NET specific framework components. |
FrameworkTests | Unit tests. |
DomainModel | Business objects used by the examples and demos. |
BindingExamples | Example and demo library - this contains a fair number of simple and more advanced examples, and should be considered the main reference for further exploration of this framework. |
Barebones | The bare-minimum "hello world" example of using this framework. |
Simple Example
I feel the best way to get started with an introduction to the code is with a simple example. I will cover the steps required to replicate the BareBones/BindingExample.aspx page and supporting classes.
The best place to start is with the creation of the ViewModel. Anyone who has spent time working with MVVM will appreciate that this is one of the most rewarding parts about working with the pattern - it encourages a measured, data/action-centric approach when developing new UI.
Hide Copy Code
[Serializable]
public class ViewModel
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime CreatedDate { get; set; }
public ClickCommand OnClick
{
get
{
return new ClickCommand();
}
}
public ViewModel()
{
//Just some default values so we see something on the screen.
//In a real world scenerio, these would be loaded from the model.
ID = 1;
FirstName = "Dave";
LastName = "Smith";
CreatedDate = new DateTime(1983, 07, 01);
}
}
Nothing complicated or scary about that. Just a simple class which exposes some properties to which we will bind. Note the
SerializableAttribute
, this is required only if we intend to useStateMode.Persist
which stores the ViewModel in the View State. Another thing that probably stands out is the ClickCommand
. This is simply an implementation of ICommand
, as follows:
Hide Copy Code
public class ClickCommand : ICommand
{
#region ICommand Members
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
}
#endregion
}
The above
ICommand
performs no action, but demonstrates how to wire a command to a method defined via the ViewModel.
Next, we set this ViewModel as the data context of the page. We do this by implementing the
IBindingContainer
interface and by calling an (extension) method from the overridden onload
event of the page's code-behind.
Hide Shrink Copy Code
//two using statements
using Binding.Interfaces;
using Binding;
namespace Barebones
{
/*Implement one interface, with one property only*/
public partial class BindingExample : System.Web.UI.Page, IBindingContainer
{
#region IBindingContainer Members
private object dataContext = new ViewModel();
public object DataContext
{
get { return dataContext; }
set { dataContext = value; }
}
#endregion
//override one method
protected override void OnLoad(EventArgs e)
{
//call one method
this.RegisterForBinding();
base.OnLoad(e);
}
}
}
The extremely straightforward code above sets the data context of the page by returning our ViewModel as the value of the
DataContext
property, and registers the page as a binding container by calling RegisterForBinding()
.
That's all the plumbing required in order to start developing using MVVM and to start harnessing two-way data binding.
The next step is to create our view (i.e., controls and some bindings).
Hide Copy Code
<%--Import 1 namespace and as far as markup is concerned you're ready to go! --%>
<%@ Import Namespace="Binding" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<!--Two-way (default) data bindings-->
ID: <asp:Label ID="lbID" runat="server"
Text='<%# sender.Bind("ID") %>'></asp:Label>
<br />
First Name: <asp:TextBox ID="tbFirstName" runat="server"
Text='<%# sender.Bind("FirstName") %>'>
We're now ready to run the example. Doing so will result in the following:
To test the above, I suggest adding a
Page_PreRender
handler to your code-behind and setting a breakpoint. Also place a breakpoint in the Execute
method of the ClickCommand
. Modify the values of the textboxes and click Submit. First the breakpoint in the Execute
method will be hit, next thePage_PreRender
one. Examine the DataContext
property (ViewModel) to see the unbound values from the textboxes. Using Page_PreRender
in this manner is the recommended approach for ViewModel/DataContext verification with all supplied examples as by this point in the PLC, everything bind-y should have run.
Something that is worth explaining, which some of you will be scratching your heads over:
sender.Bind("...")
and sender.BindC("...")
.sender
? Where? What? Why? This is to do with taking advantage of the ASP.NET data binding lifecycle which unfortunately doesn't offer much in the way of extensibility, and so this is really a hack in order for us to get a hook-in from which we can dangle the rest of our framework. There are other ways of doing this, of course: static method calls from mark-up, page base classes, event handlers, protected code-behind methods - but all these require either more code-behind, or inheriting from a common super-class which we are trying to avoid. So, where does the "sender
" come from? See the following dis-assembled method from the above example's page:
Hide Copy Code
public void __DataBindingtbFirstName(object sender, EventArgs e)
{
TextBox dataBindingExpressionBuilderTarget = (TextBox) sender;
BindingExample Container =
(BindingExample) dataBindingExpressionBuilderTarget.BindingContainer;
dataBindingExpressionBuilderTarget.Text =
Convert.ToString(BindingHelpers.Bind(sender, "FirstName"),
CultureInfo.CurrentCulture);
}
The data-binding statement we author in mark-up is executed in the scope of the above method. We also have the following extension method defined (amongst others):
Hide Copy Code
/// <summary>
/// Bind with the default options
/// </summary>
/// <param name="control" />
/// <param name="sourcePath" />
/// <returns>
public static object Bind(this object control, string sourcePath)
{
return Bind(control, new Options { Path=sourcePath });
}
So what we're doing with
sender.Bind
is simply calling an extension method of System.Object
.
This is, of course, a very simple example, but I hope it demonstrates the ease with which the supplied framework can be harnessed. Please refer to the BindingExamples project (and its menu -MainMenu.aspx) for further examples applicable to a number of disparate scenarios and for a demonstration of the features available for you to use.
Exploring the Syntax
There are three types of binding (inline)Data, (inline)Command, and (global)Resource. These are utilised by the following binding methods:
- Data:
sender.Bind()
- Command:
sender.BindC()
/sender.BindCommand()
- Resource:
sender.BindR()
/sender.BindResource()
(Where two methods are specified, one is simply a shorthand syntax of the longer version to reduce mark-up verbosity.)
Command and Databinding are self explanatory, one is for data and supports two-way, one is for commands and is one-way only. The resource binding is offered as a stand-in to WPF resources, enabling you to specify a binding once and use it across many controls. Resource bindings can be either Command or Data bindings; specify which via the
Mode
property when declaring global resources - see BindingExamples/Advanced/GridViewExample.aspx for an example of this in action.
Simple bindings are created simply with:
Hide Copy Code
Text='<%# Bind("Expression") %>'
Text='<%# BindC("Expression") %>'
Text='<%# BindR("ResourceID") %>'
There is also an extended syntax that allows a greater level of control over the binding:
Hide Copy Code
Text='<%# sender.Bind(new Options{Path="Expression",
Converter="LeaveTypeImageConverter",
Mode=BindingMode.OneWay, IsAuthorative=true})%>'
Text='<%# sender.BindC(new Options{Path="Expression",
Converter="LeaveTypeImageConverter",
Mode=BindingMode.Command, IsAuthorative=true})%>'
Text='<%# BindR("ResourceID") %>'
As you can see, the extended syntax is only available for Command and Data bindings as Resource bindings are controlled via their declaration. When specifying a
Converter
, this should be the fully-qualified type name of the converter.
A third syntax is also available, a shorthand version of the extended syntax above (seeBindingExamples/Simple/BindingOptionsExample.aspx for a demonstration):
Hide Copy Code
Text='<%# sender.Bind(new {Path = "Type",
Converter="LeaveTypeImageConverter",
Mode="OneWay", IsAuthorative=true}) %>'
Text='<%# sender.BindC(new {Path = "Type",
Converter="LeaveTypeImageConverter",
Mode="Command", IsAuthorative=true}) %>'
Text='<%# BindR("ResourceID") %>'
The supplied examples are really the best place to start seeing this syntax in action. I've tried to make it natural, intuitive, and WPF-like as possible within the constraints of ASP.NET.
StateMode - Persist or to Not Persist
When utilising this framework, you have a choice between two
StateMode
s: Persist
and Recreate
.
With
StateMode.Persist
, the DataContext
of the page will be stored in View State between page requests so that you can work against a stateful ViewModel. This is the default mode and has been implemented to mimic WPF-centric MVVM as closely as possible. To use StateMode.Persist
, the ViewModel must be [Serializable]
.
With
StateMode.Recreate
, the DataContext
of the page must be recreated on each postback and as such is the responsibility of the page developer. This could be as simple as instantiating a new instance of the ViewModel and returning it via the getter of the DataContext
property of the page.
I feel that choosing the correct
StateMode
will depend on the scenario, and so I have left it down to the consumer to choose the mode most suitable for the situation.
Controlling
StateMode
can be done on a page by page or site-wide basis. If the mode is specified for a page, it will override any site-wide settings.
To set the
StateMode
for the entire site, use the following in web.config:
Hide Copy Code
<appSettings>
<add key="BindingStateMode" value="Persist"/>
</appSettings>
Or:
Hide Copy Code
<appSettings>
<add key="BindingStateMode" value="Recreate"/>
</appSettings>
To set on a page by page basis, use a
BindingOptionsControl
(see:BindingExamples/Advanced/NestedStatelessBinding.aspx).
Hide Copy Code
<Binding:BindingOptionsControl ID="bindingOptions" runat="server" StateMode="Persist" />
Or:
Hide Copy Code
<Binding:BindingOptionsControl ID="bindingOptions" runat="server" StateMode="Recreate" />
Controlling the Unbind
You can choose between simple and automated -
UpdateSourceTrigger.PostBack
- and complete control - UpdateSourceTrigger.Explicit
- when it comes to initiating the unbind operation.UpdateSourceTrigger.PostBack
will unbind the View to the ViewModel on each postback so that the latest UI data is available anytime after Load
on every postback.UpdateSourceTrigger.Explicit
will only unbind the View when you tell it to. In order to initiate an unbind with this mode set, you should call this.Unbind()
from the page, orBinderBase.ExecuteUnbind()
from the ViewModel or ICommand
.
See DomainModel\ViewModels\RegistrationFormExample.aspx andDomainModel\ViewModels\RegistrationFormExplicitExample.aspx for examples of using these two approaches.
As with
StateMode
, the UpdateSourceTrigger
can be set either site-wide via web.config or on a page by page basis using a BindingOptionsControl
.
To set
UpdateSourceTrigger
for the entire site user:
Hide Copy Code
<appSettings>
<add key="UpdateSourceTrigger" value="PostBack"/>
</appSettings>
Or:
Hide Copy Code
<appSettings>
<add key="UpdateSourceTrigger" value="Explicit"/>
</appSettings>
To set on a page by page basis, use:
Hide Copy Code
<Binding:BindingOptionsControl ID="BindingOptionsControl1"
runat="server" UpdateSourceTrigger="PostBack" />
Or:
Hide Copy Code
<Binding:BindingOptionsControl ID="BindingOptionsControl1"
runat="server" UpdateSourceTrigger="Explicit" />
Diving Deeper
I will not try and provide a blow by blow of exactly how this framework has been constructed because this article is already getting long and my goal here is to provide a starting point, some background information , and offer some insight to some aspects I feel are less than obvious and which might bite you during your explanation/utilisation of this framework. This isn't to say I don't think there's some value in an explanation of how the framework is implemented, if people would like a follow-up article - a deep dive - then speak up and I'll get to work.
POINTS OF INTEREST
Notes on Context
When creating a binding expression, the most important piece of information to have in mind is the context to which the statement will apply. In other words, which object's properties am I targeting when I write: assignment of data sources do not count as parent bindings, in which case the context would still be the
Employee.FirstName
. In straight-forward cases, the answer to this is simple: theDataContext
of the page (your ViewModel). Life isn't always simple though, as this is not always the case. The more precise answer to "what is the context?" is the DataContext
of the page or the object returned by a parent binding, whichever is closest (most recent ancestor). The "parent binding" is only applicable if it is bound via the framework; standard ASP.NET data binding or programmatic DataContext
of the page. The packaged source contains an example showing this concept at work. Please refer to BindingExamples/Advanced/ContextExample.aspx.Testing
One of the key motivations behind the adoption of the MVVM architecture (as well as MVP and MVC implementations) is a separation of responsibility that allows for business and application logic to be placed in classes that are inherently unit testable. The draw backs of the standard WebForms postback/code-behind model are well documented and have led to the evolution of WCSF and ASP.NET MVC. Using the proposed binding framework along with an MVVM pattern will allow this same level of testability. I might even argue that it leads to greater test coverage than WCSF/MVP because of the ability to almost completely dispose of code in the code-behind, but I won't take that argument further as they both allow for very testable code.
The use of the MVVM pattern (especially when coupled with a DI container and IOC) means that you can write ViewModels that are totally decoupled from the rest of the application and as such should be trivial to write unit tests for. For more information on unit testing View Models, see: Unit testing View Models
The supplied solution contains a number of tests that are written to test the framework itself. I'm under no illusions about the coverage of these tests, I know that they're fairly limited, but I hope that they show that the framework itself, as well as the code written to utilise it, has been written in a manner that allows for the code to be fully exercised via unit tests. As this project moves forward, the expansion of this test suite is something that I plan to tackle as a priority.
Case Sensitivity
The standard matching case, but in order to preserve consistency, this case insensitivity when matching properties has been replicated for unbind operations in this framework.
Databinder.Eval
is used during bind. Databinder.Eval
ignores the case of the properties to which you bind, binding to the first property it finds with the specified name, regardless of case. Personally, I don't like this very much and would rather have the extra control afforded by explicitly Change Notification and Cascading Updates
Cascading updates are defined as: If multiple controls are bound to the same ViewModel property and the value of that property is modified by an unbind operation, then all bound controls are updated with the new value.
For example: I have a textbox and label bound to the
EmployeeName
property on the ViewModel. I enter a new name into the textbox and initiate a postback. When the page is rendered back to the client, I would also expect the label to reflect the new value.
In order to support cascading updates, the ViewModel must implement
INotifyPropertyChanged
or INotifyCollectionChanged
(for IEnumerable
). If binding via deep paths (properties of objects exposed via the ViewModel's own properties), then the underlying objects must also implementINotifyPropertyChanged
and the events must propagate to the parent (ViewModel) which in turn must raise the PropertyChanged
event. The framework also includes a custom collectionNotifyPropertyCollection<t>
which should ease development when exposing collections ofINotifyPropertyChanged
objects via the View Model. This should not be confused withSystem.Collections.ObjectModel.ObservableCollection
orSystem.Collections.Specialized.INotifyCollectionChanged
which are instead used for monitoring the state of a collection, not the properties which the items of that collection expose.
For more information on
INotifyPropertyChanged
and event propagation, see:- http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged(v=vs.85).aspx
- http://blogs.imeta.co.uk/jyoung/archive/2010/04/06/848.aspx
- http://www.codeproject.com/KB/cs/PropertyNotifyPart2.aspx
N.B.: When implementing
INotifyPropertyChanged
on objects marked as serializable
, you must apply [field: NonSerialized]
to the PropertyChanged
event declaration in order to avoid the BinaryFormater
trying to serialize the methods (and their parent objects!) that are subscribed to this event. For example:
Hide Copy Code
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
ViewModel Integrity
In order to avoid issues with data integrity, it is important to understand the mechanism used for locating the object and property (read - original source) during the unbind operation. The binding system does not use keys (IDs) to resolve a binding to an object at unbind time, instead relys on an indexed path which is stored in the View State between postbacks. The following is an example of a path:
AvailableAddresses[1].PhoneNumbers[3].AreaCode
.
This method is different to the mechanism used by stateful environments like Silverlight and WPF which have the luxury of being able to map back to the same object from which the data was initially retrieved and as such can rely on object equality, the ideal scenario. Due to the stateless nature of the web, we do not have this option in ASP.NET and so we must store a path (including the indexers if binding to collections) so that we may traverse this path at unbind time in order to ascertain where to write back the value retrieved from the View. This method, whilst effective, does introduce a responsibility which must be appreciated in order to avoid data corruption in the ViewModel. It is important that no changes are made directly to the ViewModel on a postback before the unbind is initiated. If changes are made to the ViewModel, say setting the value of a property, this value will be overwritten when you unbind. More importantly still, ensure that any collections are presented in the same order and contain the same number of items as they did when the initial bind took place. If you modified the ViewModel so that a collection contains less items then at initial bind, you are likely to receive index out of range exceptions. If the items are in a different order, then you may end up with corrupt and invalid data as values are mapped back on to the incorrect objects.
This caveat emptor applies to both
Persist
(stateful) and Recreate
(stateless) state modes, but there is a greater risk with stateless binding as the responsibility for recreation of the ViewModel is placed in the hands of the page designer, whereas with stateful binding, some of this responsibility is assumed by the binder framework as it serializes the ViewModel to View State and deserializes it into an object on each post back. Although many will feel that stateful binding is not worth the sacrifice due to View State size considerations and risk of concurrency issues (in a lot of situations, I would agree), it does minimise exposure to the potential issues described here.Notes on Reusing WPF Assemblies
I had to make a decision on whether to reuse the classes supplied by WPF or recreate them from scratch. This mainly applies to
ICommand
and ObservableCollection
but is likely to involve others as the framework expands. The disadvantage of reuse is a dependency on the WPF assemblies. The disadvantage of not using them is the duplication of objects with the same purpose, and more importantly with issues regarding portability of code (such as Command and ViewModel code) between platforms. I decided to reuse in the end because I couldn't see any practical harm, although purists may argue this point.HOW FAR ALONG ARE WE?
I'd like to stress that the code supplied isn't a finished product. It's probably a version 0.3 at most, but I've got to the stage where before I invest any more time I feel it would be useful to generate some feedback. Maybe (although I hope not) I'm totally missing the point and there's a reason why this approach hasn't been tried (or shared) before. Perhaps there are things you don't like, some things you do. Let me know!
In addition to the features listed in ...and some that didn't, there are a number of areas that require further development:
- The performance could be better, it's not telling currently, but once scaled, might be. I am aware of quite a few areas where performance of the framework could be improved.
- There is currently a limitation when binding programmatically, which really is best demonstrated by example. There is a test which demonstrates the issue in the test suite:
NestedCollectionRelativePathTest
. - The test coverage of the framework needs to be expanded significantly.
- Error handling needs improving as well, the design goal of: bindings not throwing exceptions under any circumstances, hasn't been implemented, but my excuse is that with exceptions, the code is easier to debug whilst the framework is still in a development stage....although this could be (and probably is) a cop out.
0 comments:
Post a Comment