Postulate.WinForms: a new look at data binding

0
277

I never really liked WinForms data binding out of the box. Yes I’m familiar with the Binding object that was introduced in .NET 2.0, but I always found the WinForms’ standard data binding architecture fussy and complicated. I’m happy to write a little code to implement binding, and not rely on designers, but I want that code to be really concise and intuitive. I want some sensible built-in behaviors and keyboard integration that resembles forms in Microsoft Access. I’m not at all nostalgic for Access as a dev environment, but Access did several things right — among them the data binding behavior of forms. Set aside the legitimate architectural concerns one has about Access’ file server model — I’m talking simply about how easy it was to setup and use data entry forms. That type of ease and productivity has not existed in .NET desktop apps as far as I can tell (well except maybe in LightSwitch, but that product was pulled before I had a chance to look at it), and I wanted to address that with this project.

You might be wondering why I would care today since WinForms is so five-or-more-years-ago. I go through bouts of WinForms revival every time I spend any length of time with XAML/WPF. That’s a subject for another article, but suffice it say I have hellaciously mixed feelings about WPF.

With that said, I’ll introduce Postulate.WinForms. This builds on my Postulate.Orm package (intro here) and targets WinForms specifically as the name suggests. I have a separate Mvc project which I should be talking about, but anyway — I’d like to get this off my chest. You might wonder why I hitched a data binding library to an ORM. Isn’t tight coupling bad, especially an ORM? I felt in this case, no — ease of use and robustness mattered more to me than data access layer flexibility. I felt that an ORM-neutral approach would trade away the simplicity I was after.

Postulate.WinForms has two main objects: FormBinder and GridViewBinder along with some helpers for working with combo boxes. Since there’s enough to say about FormBinder to fill this article, I’ll save GridViewBinder for another time.

Setting up

When you run the included sample app TestWinForms, it will attempt to create the database and fill it with seed data on your localdb instance. The relevant code for this is in the Main method of the application, and looks like this — it uses Postulate’s SchemaMerge feature. This should be all you need to follow along with this rest of this article.

[STAThread]
static void Main()
{
    var sm = new SchemaMerge<TdgDb>();
    sm.CreateIfNotExists((cn, db) =>
    {
        var orgs = new OrgSeedData();
        orgs.Generate(cn, db);

        var states = new StateSeedData();
        states.Generate(cn, db);
    });

    
}

Side note: This leverages some new Postulate features I haven’t documented yet: SchemaMerge.CreateIfNotExists method and the SeedData abstract class.

Crash Course

FormBinder is used to present a single record via standard controls, and to support CRUD actions like Add, Delete, and Save. It also supports UserControls that implement IFormBinderControl. It has no UI, you just use it as an instance variable within your forms. Also note that FormBinder does not support any scrolling or filtering of records; it’s always acting on a single record at a time. The following example defines a FormBinder for a Customer type with an int primary key. (The primary key type is a Postulate requirement.)

public partial class Form1 : Form
{
 private FormBinder<Customer, int> _binder = null;
 ...
}

In your form’s constructor, you new up the FormBinder instance, passing it the current form and a SqlDb instance. Then, you add controls and the related properties they bind to with a convenient lambda syntax. I’ll bet you can follow it with no explanation:

public Form1()
{
 InitializeComponent();

 _binder = new FormBinder<Customer, int>(this, new TdgDb());
 _binder.AddControl(cbOrg, c => c.OrganizationId);
 _binder.AddControl(tbFirstName, c => c.FirstName);
 _binder.AddControl(tbLastName, c => c.LastName);
 _binder.AddControl(tbAddress, c => c.Address);
 _binder.AddControl(tbCity, c => c.City);
 _binder.AddControl<string>(cbState, c => c.State);
 _binder.AddControl(tbZipCode, c => c.ZipCode);
 _binder.AddControl(tbEmail, c => c.Email);
 _binder.AddControl(chkSendNewsletter, c => c.SendNewsletter, true);
 _binder.AddRadioButtons(new RadioButtonDictionary<bool>()
 {
 { true, rbIsTaxExemptTrue },
 { false, rbTaxExemptFalse }
 }, c => c.IsTaxExempt, false);
}

A couple things to note:

  • The TdgDb is my Test Data Generator sample database from my previous CodeProject article. I added a few things to the model to flesh out my examples here.
  • When adding a combo box, the default list item value type is int. For example, cbOrg above. However, when setting up cbState combo box, I used AddControl<string> overload because Customer.State is a string.
  • Radio buttons have a dedicated method for adding in a set. You can add them individually, but the example above is the preferred way. This relies on the RadioButtonDictionary helper class, which has no other use apart from this method. This lets us map the specific value from each radio button to a desired storage value without subclassing the built-in RadioButton control.
  • There are slight differences between the code above the release copy attached to the article since I was playing around with a lot of things during the writing of the article, but I wanted the simplest code showing above.

To give you a visual reference, here’s what the form looks like when running:

I added a basic find feature in the top right so I could move around to different records and test the form’s behavior. The “label9” label near the bottom is where the record Id appears — again a testing feature. I was too lazy to change the default text. The yellow banner at the bottom is a ValidationPanel control, which is included in this package and used to display error messages and convey the record status. We’ll cover that a bit more in a bit.

The only other thing we need is fill the combo boxes in the form’s Load event. The binder automatically calls AddNew method when the form loads, effectively starting the form with a blank record. (Unlike built-in WinForms data binding, the AddNew method does not “dirty” the form. Rather it simply clears the form or applies default values. When you type the first character in the first field, then the record becomes “dirty.” This is Access behavior I wanted to mimic.)

private void Form1_Load(object sender, EventArgs e)
{
 cbOrg.Fill(new OrgSelect());
 cbState.Fill(new StateSelect());
}

The combo box Fill method deserves some explanation, though it’s a slight detour from the data binding discussion proper. Fill is an extension method for filling combo boxes in a way that avoids inline SQL, and encapsulates some database details. This technique uses my Postulate.Sql package combined with Postulate.WinForms.ListItem<TValue>. Let’s take a quick look at OrgSelect:

public class OrgSelect : Query<ListItem<int>>
{
 public OrgSelect() : base(
 "SELECT [Id] AS [Value], [Name] AS [Text] FROM [dbo].[Organization] ORDER BY [Name]",
 () => new TdgDb().GetConnection())
 {
 }
}

This gives us a way to execute familiar SQL statements while keeping the SQL itself isolated — as well as offering some unit test capability (via the Query<>.Test method). The Query<> constructor requires the SQL itself followed by a delegate for returning the database connection. This lets you invoke the query without needing a using block around a database connection. (There’s an overload that accepts an open connection if you happen to have one in scope.)

In a real application I’d simplify this a bit further by hiding the delegate argument in my own Query<> subclass — then I’d derive all my queries from that subclass. This is because, presumably, all my queries would hit the same database, so the subclass would reuse the same connection delegate without my having to specify on every query.

Lastly, note that I’m using type ListItem<TValue> as the query result type. The Fill method expects this. This provides a standard way to define a value and text pair for populating a combo box list. If you need to add more data to your list items for some reason, you can inherit from ListItem<TValue> and add your own properties. The one requirement to bear in mind is that the query you use must return Value and Text column names as you see above since ListItem<TValue> has those property names. The Value column data type is flexible, corresponding to the TValue generic argument.

Before we dive into how FormBinder works, let’s review what you need to make it work:

  • Declare a FormBinder instance variable for your form
  • In your form’s constructor, after the InitializeComponent() call, new up the FormBinder instance and follow it with your specific control bindings with the AddControl method.
  • In the form’s load event, fill any combo boxes, if any.

Code Tour: CheckBoxes

Although I’ve tinkered with custom data binding solutions for years, I found it pretty hard to make everything work the way I wanted for this article. I’m still not really sure how to test it properly, and I may seek some professional help with that. But all that aside, let me walk you through what it does. Like I say some of it was surprisingly hard, and I tried very hard to keep the code simple and self-documenting.

Let’s start with a simple control — the CheckBox. TextBoxes, ComboBoxes, and RadioButtons for example all have some oddities, so the CheckBox is a good starting point. This example binds a CheckBox called chkSendNewsletter to a Customer property SendNewsletter, with a default value of true, causing the box to be checked on new records.

_binder.AddControl(chkSendNewsletter, c => c.SendNewsletter, true);

Let’s look at the relevant AddControl method overload:

public void AddControl(CheckBox control, Expression<Func<TRecord, bool>> property, bool defaultValue = false)
{
 PropertyInfo pi = GetProperty(property);
 Action<TRecord> setProperty = (record) =>
 {
 pi.SetValue(record, control.Checked);
 };

 var func = property.Compile();
 Action<TRecord> setControl = (record) =>
 {
 control.Checked = func.Invoke(record);
 };

 AddControl(control, setProperty, setControl, defaultValue);
}

When you think about what data binding entails, we need two things to happen: set a property value in response to a control event, and likewise set a control value from another property value. (A step further to go is to update a control continuously in response to changes in another object property. I haven’t done that — I am setting a control value once from a record loaded from a database. You’d have to load the record from the database again to see an updated value.) So, in essence we have two delegates — one for setting a control, and one for setting a property.

The two delegates are derived from this single argument in the method: Expression<Func<TRecord, bool>> property. You might recognize this kind of syntax from the HTML helper methods in MVC Razor views (e.g. Html.TextBoxFor, Html.CheckBoxFor, etc). This is sometimes called “static reflection,” and it blew my mind when I first saw it in MVC. The Expression<> part lets us describe a delegate without calling it. That’s useful in the way that anything “declarative” or “descriptive” is useful: we can say what we want without getting lost in how to get it.

The last line of this method calls another overload of AddControl. I did that because this project evolved from an earlier version where I had to set the property and control delegates explicitly. I didn’t like that because it was extra coding (and it wasn’t good enough for a CodeProject article), but I had to go one step at a time and work from what I knew. I kept the earlier overload because it might be useful on its own if you need more control of the process, but more importantly, it reduced complexity to keep the original overload:

public void AddControl(
  CheckBox control, Action<TRecord> setProperty, Action<TRecord> setControl, 
  bool defaultValue = false)
{
 control.CheckedChanged += delegate (object sender, EventArgs e) { ValueChanged(setProperty); };
 _setControls.Add(setControl);
 _setDefaults.Add(new DefaultAction<TRecord, TKey>()
 {
 SetControl = () => { control.Checked = defaultValue; },
 SetProperty = setProperty, InvokeSetProperty = defaultValue
 });
}

The first line says, in effect: whenever the checked state changes, indicate that a bound value has changed (via the ValueChanged method, marking the form “dirty”) and as part of that, call the delegate that sets the bound property.

The next statement says: add the setControl delegate for setting a control to the list of delegates that is called whenever we load or undo a record. This means that the Checked property is going to be set from the designated property of the incoming data record.

The last line is less obvious, but it supports the behavior of default values. Default values were harder than I expected to get working. Setting a default value can affect both a control and a property in one step outside the normal end user event order. That’s why DefaultAction has two properties SetProperty and SetControl — I needed to pass both delegates to the default handler within the AddNew method. The InvokeSetProperty property is sort of a wrinkle — it lets me indicate whether I actually need to set a property or not. For defaults that merely blank out controls, I don’t need to set a corresponding property since a new record already has blank properties. Since “blank” means something different to different controls, I had to be explicit about setting a property or not. For example, a “blank” ComboBox has a SelectedIndex of -1. But that -1 is not meaningful at the model level, so it doesn’t correspond to a property setting, only a control setting.

To sum up, the essence of my approach is for each control to have two delegates — one for setting a property from a control, the other for setting a control from a property — and to be able to invoke them in response to control and binder events and actions. The delegates can be set explicitly, but it’s more elegant if they can inferred from an Expression<>. If that makes sense, we’re ready to look at TextBoxes, which have more subtleties than CheckBoxes.

TextBoxes

From a data binding perspective, TextBoxes are annoying because you have to watch two events instead of one: Validated and TextChanged. Validated happens essentially every time you tab through a field whether you made changes or not. TextChanged fires with every individual character change. For smooth data binding, what we really need is a Validated event that fires only after you made changes. A further wrinkle is the Validated event doesn’t fire unless you leave a field. So, if you try to Save a record, and you’re in the last field of a form that you’ve made changes to, but not tabbed out of, the setProperty delegate never gets called, and so it appears to the model class like you left the last field empty. Therefore, I need some way to validate TextBoxes manually during a save, and the built-in support for manual validation is pretty wonky. With these cautions in mind, let’s look at the AddControl overload for TextBoxes:

public void AddControl(
 TextBox control, Action<TRecord> setProperty, Action<TRecord> setControl,
 object defaultValue = null, EventHandler afterUpdated = null)
{
 EventHandler validated = delegate (object sender, EventArgs e)
 {
 if (_textChanges[control.Name])
 {
 ValueChanged(setProperty);
 _textChanges[control.Name] = false;
 _validated[control.Name] = true;
 afterUpdated?.Invoke(control, new EventArgs());
 }
 };

 _textBoxValidators.Add(control.Name, new TextBoxValidator(control, validated));
 _textChanges.Add(control.Name, false);

 InitDitto(control, setProperty, setControl);

 control.TextChanged += delegate (object sender, EventArgs e)
 {
 if (!_suspend)
 {
 _textChanges[control.Name] = true;
 _validated[control.Name] = false;
 }
 };

 control.Validated += validated;

 _setControls.Add(setControl);
 _setDefaults.Add(new DefaultAction<TRecord, TKey>()
 {
 SetControl = () => { control.Text = defaultValue?.ToString(); },
 SetProperty = setProperty,
 InvokeSetProperty = defaultValue != null
 });
}

From the top — the first line creates a standalone Validated event handler.

EventHandler validated = delegate (object sender, EventArgs e)
{
 if (_textChanges[control.Name])
 {
 ValueChanged(setProperty);
 _textChanges[control.Name] = false;
 _validated[control.Name] = true;
 afterUpdated?.Invoke(control, new EventArgs());
 }
};

Since I need to validate TextBoxes both in response to the regular Validated event, but also manually sometimes during a Save, I need a single delegate that can work in both places. It says, basically, if my text has changed, then signal the binder that it’s “dirty” and set the bound model property. Then, reset the tracking on text changes for this field to false, but indicate that validation has happened. As a bonus, invoke the custom afterUpdated event if specified. (Again, since there’s no built-in AfterUpdate event like Access had, I thought it would be nice to sneak one in, but I didn’t want to subclass the TextBox control and get all official about it.)

_textBoxValidators.Add(control.Name, new TextBoxValidator(control, validated));

This line associates the control with the validation delegate I just created. Remember, I will need to validate some text boxes manually during a save, but how will I find the validation delegate when I need it during the save? The answer is a dictionary keyed to the control name, and that’s what this is here.

_textChanges.Add(control.Name, false);

I need a place to track which text boxes have fired TextChange events, and a dictionary is a natural place for that.

InitDitto(control, setProperty, setControl);

Any credible data entry form supports easy copying of values from the previous record to the current field. I won’t drill into the details here, but this sets up Enter and Leave events on the control so the binder can track the current field. A keystroke recognized by binder’s KeyDown handler invokes this “ditto” feature.

control.TextChanged += delegate (object sender, EventArgs e)
{
 if (!_suspend)
 {
 _textChanges[control.Name] = true;
 _validated[control.Name] = false;
 }
};

In addition to watching the Validated event, we also have to watch the TextChanged event of course. This says, basically, if we’re not ignoring user actions now (i.e. !_suspend), then set flags that the text has changed, but validation has not happened yet. Why would would we ever “ignore user actions”? If we’re setting control values during a Load or undoing changes, then those are binder actions — not user actions. We don’t want to mistake what the binder does for user actions since that would really confuse things.

control.Validated += validated;

Remember the standalone validation event we created to start with? Here, I’m applying it to the Validated event so it can fire in response to that event “naturally.”

_setControls.Add(setControl);

Whenever the binder loads a record, it needs to know what exactly is being set on each control, so I have a list of actions (delegates) that are saved up for when we do a load. This would be an action that simply sets the TextBox.Text property from the related record property. Jump ahead here to see what I mean.

_setDefaults.Add(new DefaultAction<TRecord, TKey>()
{
 SetControl = () => { control.Text = defaultValue?.ToString(); },
 SetProperty = setProperty,
 InvokeSetProperty = defaultValue != null
});

Lastly, this sets up the TextBox’s default behavior. This says: set the control text to whatever the defaultValue argument was that was passed in. If it’s not null, then set the related model property as well.

At this point you might wonder: Where’s the Expression<> overload that infers the setProperty and setControl delegates automatically? Don’t worry, that overload is there, I just started from the ground up in the TextBox discussion. You can absolutely use the easier lambda syntax to setup up TextBox binding.

Using the FormBinderToolStrip control

I got quite a ways into this project when I realized I was using only shortcut keys interact with the FormBinder, and that I needed a toolbar control since that would be more familiar to people. You can add the FormBinderToolStrip to your toolbox and then drop it onto forms as you like. The only built-in buttons are new, save, and delete. I’ve added some Find controls just to make my sample app a bit more usable.

You have to add one line of code in your form constructor to make the toolbar work:

_binder.ToolStrip = formBinderToolStrip1;

Conclusion

I didn’t intend to get carried away with a WinForms project, but I actually really enjoyed it once I got past several difficulties. Like I said I never really liked the standard data binding model for WinForms in .NET. But this is really the first time I’ve felt like I had a real alternative. I did create a nuget package for it Postulate.WinForms that I will happily make improvements to if there’s any interest. I realize I’m likely to be the only one interested!

I regret that I didn’t get around to talking about ComboBoxes and RadioButtons, since RadioButtons in particular have an interesting data binding story. I addressed it by passing RadioButtons in a special dictionary, and treating the dictionary as a single control. My hope is you can follow the code easily how radio buttons are added to the binder, but I welcome questions and feedback.

LEAVE A REPLY