Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Declarative Programming Of The MVC Pattern Within The Context Of DataBinding

0.00/5 (No votes)
31 May 2004 1  
Exploring the MVC pattern.

Introduction

The Model-View-Controller (MVC) design pattern is a powerful way to separate the concerns of the user interface, the data model, and the state management of both. Events are used to achieve this decoupling. Both the model and view are well suited for declarative programming. The model manages the data, being a collection of fields with a known type, and the view is a representation of the user interface as expressed by a collection of controls. The controller is best constructed in code, as it contains the logic for responding to the events fired by the view and adjusting the model's state.

In a classic MVC pattern, the view does not directly change the model's state (as in, field values managed by the model). Instead, it fires events to the controller. The controller contains the logic to decide how the model state changes and informs the model of the appropriate state change with a direct function call. The model, upon processing the state change, fires an event which is received by the view(s). The view adjusts the state of the UI to reflect the state change of the model.

When data binding is employed, the model and view have a direct communication path that is managed by the underlying framework rather than the application. If the view changes, the field's value in the model is automatically updated. Conversely, if the field's value in the model changes, the view is automatically updated. The controller never intervenes unless the Binding class' Parse event is handled. However, this event is intended to provide custom unformatting of displayed values rather than state management, so it's not the appropriate use of the event.

A Working Example

I am going to illustrate the MVC pattern using MyXaml as the declarative syntax. A simple example is a date-range picker, which is a common enough dialog used to select the date ranges of reports:

Declaratively Creating The Model

The model for this UI manages the following fields:

  1. start date
  2. end date
  3. selected default date range option
  4. group by index

This is expressed declaratively by constructing a container in the markup:

<MxContainer Name="DateSelection">
  <MxProperties>
    <MxProperty Name="StartDate" Type="DateTime"/>
    <MxProperty Name="EndDate" Type="DateTime"/>
    <MxProperty Name="Option" Type="int"/>
    <MxProperty Name="GroupBy" Type="int"/>
  </MxProperties>
</MxContainer>

Now, we have a problem. Data binding works with fields only through corresponding property "get" and "set" methods. We can't abstract this concept with data binding, so instead, we have to generate at runtime the code for the class that represents this container and instantiate it. The MxContainer class, when instantiated by the MyXaml parser and all the parsing of the class is complete, does exactly this. The following code is auto-generated:

// MyXaml Runtime Generated Code

using System; 
using System.Collections; 
using MyXaml.Extensions; 
namespace MyXaml.App.Container 
{ 
 public class DateSelection : MxDataContainer 
 { 
  protected DateTime startDate; 
  protected DateTime endDate; 
  protected int option; 
  protected int groupBy; 
  
  public DateTime StartDate 
  { 
   get {return startDate;} 
   set {startDate=value;} 
  } 

  public DateTime EndDate 
  { 
   get {return endDate;} 
   set {endDate=value;} 
  } 

  public int Option 
  { 
   get {return option;} 
   set {option=value;} 
  }  

  public int GroupBy 
  { 
   get {return groupBy;} 
   set {groupBy=value;} 
  } 
 } 
}

Initial values must also be established if they are not defined in the MxProperty element, otherwise the data binding fails for objects such as DataTime. This also is handled by the MxContainer class. The resulting instance of the DateSelection class is put into the parser's object pool. Later on, I will discuss why the class is derived from MxDataContainer.

Declaratively Creating The UI

The user interface is generated with the typical declarative syntax:

<Style def:Name="RangeStyle">
  <StyleProperties>
    <PropertyStyle Class="MxRadioButton" Size="80, 20"/>
  </StyleProperties>
</Style>

<Controls>
  <Label Location="10, 10" Size="220, 15" Text="Select A Date Range"
            TextAlign="MiddleCenter"/>
  <Label Location="10, 42" Size="70, 15" Text="Start Date:"/>
  <Label Location="10, 67" Size="70, 15" Text="End Date:"/>
  <DateTimePicker Location="80, 40" Size="100, 20" Format="Short">
    <DataBindings>
      <MxDataBinding PropertyName="Value" DataSource="DateSelection"
                        DataMember="StartDate"/>
    </DataBindings>
  </DateTimePicker>
  <DateTimePicker Location="80, 65" Size="100, 20" Format="Short">
    <DataBindings>
      <MxDataBinding PropertyName="Value" DataSource="DateSelection"
                        DataMember="EndDate"/>
    </DataBindings>
  </DateTimePicker>
  <GroupBox Location="10, 95" Size="220, 110" Text="Default Options:">
    <Controls>
      <MxRadioButton Location="10, 20" Size="80, 15" Text="Today">
        <DataBindings>
          <MxDataBinding PropertyName="Index" DataSource="DateSelection"
                            DataMember="Option"/>
        </DataBindings>
      </MxRadioButton>
      <MxRadioButton Location="10,40" Text="This week" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="10,60" Text="This month" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="10,80" Text="This year" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="120,20" Text="Yesterday" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="120,40" Text="Last week" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="120,60" Text="Last month" MxStyle="{RangeStyle}"/>
      <MxRadioButton Location="120,80" Text="Last year" MxStyle="{RangeStyle}"/>
    </Controls>
  </GroupBox>
  <GroupBox Location="235, 95" Size="100, 110" Text="Group By:">
    <Controls>
      <MxRadioButton Location="10, 20" Size="70, 20" Text="Hour">
        <DataBindings>
          <MxDataBinding PropertyName="Index" DataSource="DateSelection"
                                                 DataMember="GroupBy"/>
        </DataBindings>
        </MxRadioButton>
      <MxRadioButton Location="10, 40" Size="70, 20" Text="Day"/>
      <MxRadioButton Location="10, 60" Size="70, 20" Text="Month"/>
      <MxRadioButton Location="10, 80" Size="70, 20" Text="Year"/>
    </Controls>
  </GroupBox>
  <Button def:Name="OKButton" Location="250, 10" Size="80, 25" Text="OK"
                                 FlatStyle="System" Click="OnDlgOK"/>
  <Button def:Name="CancelButton" Location="250, 35" Size="80, 25"
             Text="Cancel" FlatStyle="System" Click="OnDlgCancel"/>
</Controls>

Data Binding Problems

Unfortunately, data binding comes with its own set of problems, some of which I am illustrating here.

The first and by far most annoying problem is when using data binding with regards to a radio button. Radio buttons are unique because, as the programmer, we're almost always interested in which button in the group was selected. Data binding only offers us the ability to bind to each property of each individual button. This means that we would need separate properties in the container, and this defeats the index-like quality of the radio button. To solve this problem, the MxRadioButton is a class that extends the RadioButton functionality by implementing an Index property. By binding to this property, the MxRadioButton class intelligently sets the Checked state of the appropriate radio button in the group.

Second, when the Click event fires on the RadioButton, the data binding update has not yet occurred! This means that we have to inspect the value of the control directly, rather than the property value being managed by the container. Again, to deal with this problem, the base class of the container, MxDataContainer, forces a transfer of the data to and from the control when we respectively set and get the value of the property in the container.

The good thing is, once these fixes are in place, they can be used over and over again in all of our applications.

Container Problems

The container is generated at runtime, so your compile-time code doesn't know anything about it. This means that you have to use a separate vehicle for accessing the properties in the container. Hence the reason for the container's base class, MxDataContainer. This is a class known at your application's compile time, and can be used to access the properties of the runtime compiled container. While awkward, it also has an advantage--the container's data representation is decoupled from the controller, allowing the two to vary independently, which has some advantages.

The Controller

In our controller for this UI, we want to do two things:

  1. When a default date range is selected, automatically adjust the beginning and ending dates.
  2. When the user changes the beginning or ending date, "uncheck" all radio buttons in the default range group.

Handling User Gesture Events

There are a variety of ways that this can be done, but in keeping with the MVC pattern, we want the view decoupled from the controller. In other words, the view doesn't call a method in the controller. Ideally, the view should consume any Control events and provide the controller with a "sanitized" version of the event. If the controller were to directly consume the Control's event, then a level of decoupling would be lost. This would become apparent if the view were to change and the new Control's event had a different EventArgs signature. This would break the controller's implementation.

The Event Pool that I've written about in a previous article is a nice way of managing this requirement. It also has several advantages:

  • it documents the producers and consumers of events;
  • it instruments the events;
  • it provides a safe mechanism for invoking the events, regardless if there is a consumer or not.

The question then remains, does the view pass along any data to the controller? We could easily put selection information into the Tag property of the radio buttons. But we already have a property in the container that can be used to get the current selection, so it seems more logical (and simpler) to use the container. The drawback to this is that it assumes that the property in the container will always have a data binding to the control, however it is implemented. This decision will have to be left to the programmer, based on what his/her crystal ball says regarding the possible views that might be encountered in the future. But, since the Control class, from which all controls are derived, contains the DataBindings collection, it's reasonable to say "always use data binding with a property in the container". That said, we probably don't ever have to worry about whether the view needs to pass parameters to the controller--all the relevant information should be in the container model.

Mapping Control Events

As a result of the decisions made regarding handling Control events, it would be nice if we could just map the Control event to an appropriate user gesture event managed by the EventPool. Yes, this seems like an unnecessary step in such a trivial example, but the beauty of it lies in the fact that, once handled by the EventPool, the event is instrumented! This means that we now have an automatic trace of user actions regarding what the guy in QA did, when upon being asked "how did you break it?", he replies "I dunno". This single feature has reduced QA acceptance time by two-thirds in projects on which I have worked. So, even for something that seems like unnecessary complication, there are great benefits.

Because each radio button represents a different state that we want to set in the model, it makes sense to create a separate event handler for each Click event from the RadioButton control. Therefore, we don't need to inspect the Option property of the container and we avoid a switch statement.

Since the event pool implements multicast delegates, each event managed by the pool can have more than one subscriber. Again, we can use XML to declaratively establish the connection between the Control event in the View, the corresponding EventPool event, and the handler that subscribes to the event. The ControlEvents collection handles the mapping of the control event to the corresponding EventPool event. The Subscribers collection manages the mapping of the EventPool event to one or more subscribers.

<ev:EventPool def:Name="DateRangeEventPool">
  <ev:ControlEvents>
    <ev:ControlEvent Control="{rbToday}"
                        Event="Click" MapTo="SelectToday"/>
    <ev:ControlEvent Control="{rbWeek}"
                        Event="Click" MapTo="SelectThisWeek"/>
    <ev:ControlEvent Control="{rbMonth}"
                        Event="Click" MapTo="SelectThisMonth"/>
    <ev:ControlEvent Control="{rbYear}"
                        Event="Click" MapTo="SelectThisYear"/>
    <ev:ControlEvent Control="{rbYesterday}"
                        Event="Click" MapTo="SelectYesterday"/>
    <ev:ControlEvent Control="{rbLastWeek}"
                        Event="Click" MapTo="SelectLastWeek"/>
    <ev:ControlEvent Control="{rbLastMonth}"
                        Event="Click" MapTo="SelectLastMonth"/>
    <ev:ControlEvent Control="{rbLastYear}"
                        Event="Click" MapTo="SelectLastYear"/>
    <ev:ControlEvent Control="{StartDate}"
                        Event="ValueChanged" MapTo="CustomDate"/>
    <ev:ControlEvent Control="{EndDate}"
                        Event="ValueChanged" MapTo="CustomDate"/>
  </ev:ControlEvents>
  <ev:Subscribers>
    <ev:Subscriber Event="SelectToday" Instance="{DateRangeController}"
                      Handler="OnSelectToday"/>
    <ev:Subscriber Event="SelectThisWeek" Instance="{DateRangeController}"
                      Handler="OnSelectThisWeek"/>
    <ev:Subscriber Event="SelectThisMonth" Instance="{DateRangeController}"
                      Handler="OnSelectThisMonth"/>
    <ev:Subscriber Event="SelectThisYear" Instance="{DateRangeController}"
                      Handler="OnSelectThisYear"/>
    <ev:Subscriber Event="SelectYesterday" Instance="{DateRangeController}"
                      Handler="OnSelectYesterday"/>
    <ev:Subscriber Event="SelectLastWeek" Instance="{DateRangeController}"
                      Handler="OnSelectLastWeek"/>
    <ev:Subscriber Event="SelectLastMonth" Instance="{DateRangeController}"
                      Handler="OnSelectLastMonth"/>
    <ev:Subscriber Event="SelectLastYear" Instance="{DateRangeController}"
                      Handler="OnSelectLastYear"/>
    <ev:Subscriber Event="CustomDate" Instance="{DateRangeController}"
                      Handler="OnCustomDate"/>
  </ev:Subscribers>
</ev:EventPool>

To make this all work, we again rely on runtime code generation to create the appropriate bridge class. The emitted code looks like this:

// MyXaml Runtime Generated Code 

using System; 
using System.Collections; 
using MyXaml.EventManagement; 
using MyXaml.Extensions; 
namespace MyXaml.App.ControlEvents 
{ 
  public class DateRangeEventPool : IEventPool 
  { 
    protected EventPool eventPool; 

    public EventPool EventPool 
    { 
      get {return eventPool;} 
      set {eventPool=value;} 
    } 
 
    public void Click_SelectToday(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectToday"); 
    } 
 
    public void Click_SelectThisWeek(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectThisWeek"); 
    } 
 
    public void Click_SelectThisMonth(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectThisMonth"); 
    } 
 
    public void Click_SelectThisYear(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectThisYear"); 
    } 
 
    public void Click_SelectYesterday(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectYesterday"); 
    } 
 
    public void Click_SelectLastWeek(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectLastWeek"); 
    } 
 
    public void Click_SelectLastMonth(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectLastMonth"); 
    } 
 
    public void Click_SelectLastYear(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("SelectLastYear"); 
    } 

    public void ValueChanged_CustomDate(object sender, EventArgs e) 
    { 
      eventPool.FireEvent("CustomDate"); 
    } 
  } 
}

At this point, you are probably thinking that Marc has really gone off his rocker this time! He's managed to create a bunch of time-consuming runtime generated code that replaces the very simple mechanism in .NET of wiring up an event to a handler!

And, in a sense, you are correct.

However, let's look at what all of this does:

  • I have eliminated the need to, in code, wire up events. OK, MyXaml did that already.
  • All View events are now instrumented. Cool! I can track what I do, and why the program breaks!
  • In .NET, the wiring up of the event requires that the code has both knowledge of the Control class and the instance (the Controller) that handles the event. I have eliminated that coupling. Ahhh! Now, MyXaml did that already, but the event signature was bound to the Control type. This is more abstract.
  • It creates an architectural pattern that helps me write better code. The Controller now represents a set of methods that manages the Model state. Typical implementations embed the model state management as part of the subclassed Form, at least, if you follow Visual Studio's way of generating event handlers. The "Microsoft Way" makes it impossible to implement an MVC pattern, results in costly code rewrites when the View changes, and in general, bogs down the flexibility of the application.

The Controller implementation is straight forward, which we'll add as in-line code to the markup so we get a complete stand-alone package (yes, I wrote this code first as an assembly, debugged it, then copied it into the markup):

<def:Code language="C#">
<reference assembly="System.Drawing.dll"/>
<reference assembly="System.Windows.Forms.dll"/>
<reference assembly="myxaml.dll"/>
<![CDATA[
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

using MyXaml;
using MyXaml.Extensions;

public class DateRangeController
{
  protected Parser parser;
  protected MxDataContainer dateSelection;
  protected TimeSpan oneDay;

  public DateRangeController()
  {
    parser=Parser.CurrentInstance;
    parser.AddReference("DateRangeController", this);
    oneDay=new TimeSpan(1, 0, 0, 0);
  }

  public void DateSelectionLoaded(object sender, EventArgs e)
  {
    dateSelection=(MxDataContainer)parser.GetReference("DateSelection");
  }
  
  public void OnSelectToday(object sender, EventArgs e)
  {
    dateSelection.SetValue("StartDate", DateTime.Now);
    dateSelection.SetValue("EndDate", DateTime.Now);
  }

  public void OnSelectThisWeek(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dateSelection.SetValue("EndDate", dt);
    while (dt.DayOfWeek != 0)
    {
      dt-=oneDay;
    }
    dateSelection.SetValue("StartDate", dt);
  }

  public void OnSelectThisMonth(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dateSelection.SetValue("EndDate", dt);
    dt=new DateTime(dt.Year, dt.Month, 1, dt.Hour, dt.Minute, dt.Second);
    dateSelection.SetValue("StartDate", dt);
  }

  public void OnSelectThisYear(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dateSelection.SetValue("EndDate", dt);
    dt=new DateTime(dt.Year, 1, 1, dt.Hour, dt.Minute, dt.Second);
    dateSelection.SetValue("StartDate", dt);
  }

  public void OnSelectYesterday(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dt=dt - oneDay;
    dateSelection.SetValue("StartDate", dt);
    dateSelection.SetValue("EndDate", dt);
  }

  public void OnSelectLastWeek(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    while (dt.DayOfWeek != 0)
    {
      dt-=oneDay;
    }
    dateSelection.SetValue("EndDate", dt);
    dt-=new TimeSpan(7, 0, 0, 0);
    dateSelection.SetValue("StartDate", dt);
  }

  public void OnSelectLastMonth(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dt=new DateTime(dt.Year, dt.Month, 1, dt.Hour, dt.Minute, dt.Second);
    dt=dt.AddMonths(-1);
    dateSelection.SetValue("StartDate", dt);
    dt=new DateTime(dt.Year, dt.Month, DateTime.DaysInMonth(dt.Year, dt.Month),
                    dt.Hour, dt.Minute, dt.Second);
    dateSelection.SetValue("EndDate", dt);
  }

  public void OnSelectLastYear(object sender, EventArgs e)
  {
    DateTime dt=DateTime.Now;
    dt=new DateTime(dt.Year, 1, 1, dt.Hour, dt.Minute, dt.Second);
    dt=dt.AddYears(-1);
    dateSelection.SetValue("StartDate", dt);
    dt=new DateTime(dt.Year, 12, 31, dt.Hour, dt.Minute, dt.Second);
    dateSelection.SetValue("EndDate", dt);
  }

  public void OnCustomDate(object sender, EventArgs e)
  {
    // if the container data is different from the control data, then the

    // user is changing the control data.

    string dtStart1=
      ((DateTime)dateSelection.GetContainerValue("StartDate")).ToShortDateString();
    string dtStart2=
      ((DateTime)dateSelection.GetValue("StartDate")).ToShortDateString();
    string dtEnd1=
      ((DateTime)dateSelection.GetContainerValue("EndDate")).ToShortDateString();
    string dtEnd2=
      ((DateTime)dateSelection.GetValue("EndDate")).ToShortDateString();
    if ( (dtStart1 != dtStart2) || (dtEnd1 != dtEnd2) )
    {
      dateSelection.SetValue("Option", -1);
    }
  }
}
]]>
</def:Code>

A Different View

Now, none of this would be of much value if I didn't demonstrate changing the view without affecting the model or the controller. So, in this UI:

I have replaced the radio buttons with a ComboBox. Now, this begs the question--where should we put the logic for figuring out which action to take when the user makes a selection? Does this go in the View or the Controller? There really isn't that good of an answer--in this particular case, I've decided to demonstrate both. However, I feel that since the combobox is an active selection that changes the UI state, the View should be responsible. The View manages the list and applies meaning to the list contents, and it also doesn't require changing the Controller code. I've demonstrated both approaches in the download.

The elegance of separating the View from the Model/Controller is seen in the next example. Swapping out:

<ComboBox def:Name="cbOption" Location="10, 20" Size="200, 20"
             SelectionChangeCommitted="OnSelectChangeCommitted">
  <Items>
    <Item>Today</Item>
    <Item>This Week</Item>
    <Item>This Month</Item>
    <Item>This Year</Item>
    <Item>Yesterday</Item>
    <Item>Last Week</Item>
    <Item>Last Month</Item>
    <Item>Last Year</Item>
  </Items>
  <DataBindings>
    <MxDataBinding PropertyName="SelectedIndex" DataSource="DateSelection"
                      DataMember="Option"/>
  </DataBindings>
</ComboBox>

with:

<ListBox def:Name="cbOption" Location="10, 20" Size="200, 80"
            SelectedIndexChanged="OnSelectedIndexChanged">
etc...

and we have changed the View, independent of the Controller:

Conclusion

The MVC pattern is very powerful. By using declarative programming, a lot of the drudgery in creating classes and wiring up events can be eliminated. The examples I've illustrated here are somewhat trivial. In a complicated UI, however, where a single Form may have multiple Views that are being managed, or the selection in one View affects the state in another, this technique becomes very useful. Also note that in the examples I've provided, I've put the Model, View and Controller code in the same file as the markup. This creates a physical binding between the three, which in real life you would probably not do. Finally, this is very much a concept piece. I'm using this technique for my own application development, and I suspect that it will mature further as time goes on.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here