views:

576

answers:

3

I have a form in my application that uses a binding source to bind the controls to a model object. This works, and handles basic validation such as ensuring that the user doesn't enter text into a numeric field, for example. I am using an ErrorProvider, and setting its datasource = the binding source in order to display the error messages to the user, and that works as well.

However, I am having trouble figuring out 2 things:

  1. How to override the default error message with something more intuitive to the end user. For instance the default message in the above mentioned scenario is: "Input String was not in a Correct Format" and I would prefer it to say something like: "Age must be a positive integer."

  2. How to easily extend this system with more complex business logic. For instance, the default handler above will prevent accepting a string as a number, but won't enforce the age being > 0 by default. I know I can override the onValidate for each control, but there seems like there ought to be a simpler way, that relies on the data model, and not the individual control.

I tried updating my setters as follows, hoping that the message would then show up on all the forms/bindings that use this model object, but this does not work:

Public Property Age() As Integer
  Get
    Return m_age
  End Get
  Set(ByVal Value As Integer)
    If Not IsNumeric(Value) And Value > 0 Then
      Throw New ArgumentException("Age must be a positive integer.")
    End If
    m_age = Value
  End Set
End Property

Other ideas?

+1  A: 

So from what I can tell, this is almost possible, but due to some screwy behavior, can't be as straightforward as you would like. The problem stems from the fact that the formatters and typeconverters swallow/override your domain object's error messages.

This is as close as I've gotten with this:

For the domain object you are binding to:

  • Implement System.ComponentModel.IDataErrorInfo, returning the user friendly messages you want.
  • Implement INotifyPropertyChanged to alert the binding source of changes.

Leave the ErrorProvider.DataSource set to the BindingSource.

Add a handler for the Validating event for each text field where you want to override the default conversion error message.

Add bindings for your controls manually, so that you can turn formatting off. If you don't do this, the formatter will eat any custom error messages you provide. So don't use the property editor in VS. Instead, assign the binding ala:

TextBox1.DataBindings.Add("Text", BindingSource1, "Age", False, DataSourceUpdateMode.OnValidation)

If you do the above, then your end user will see your model objects' error messages correctly so long as conversion did not cause an error. If conversion causes an error, they will see the message you provided in your Validating handler, if there was one, or the generic message otherwise.

Still not an idea solution, but this was as close as I could get. I say not ideal because this solution precludes you using automatic formatting, and requires that you duplicate some of your validation messages on different forms if you use the model object in multiple places.

A: 

Throw the error back to the caller. Then, catch the exception type that you want to "override". Instead of showing that exception message, throw a custom exception and show its message instead.

HardCode
A: 

Here. This should save you a ton of work.

In short: to validate a bound control, there are three events you want to handle.

  • The Control.Validating event, which validates the data when the user leaves the control,
  • The Control.Validated event, which updates the data source if and only if it's properly validated, and
  • The Binding.Parse event, which validates the data when anything else changes the data in the control (i.e. your code).

To insure that only valid data gets written to your data source, automatic updating of the bound data is turned off when the Binding is created - this way, data only gets written to the data source during the Control.Validated event.

Both the Validating and the Parse events put the validation error message into an ErrorProvider attached to the bound Control; you may have some other way that you want to present error messages, in which case you'll need to change both of those events.

You also may want to handle the binding's Format event, to control how the data in the field is presented in the bound control.

I haven't rewritten this code to make it generic, because I don't want to introduce any errors. So I'll explain what fc and cm are, insofar as you need to know to get this to work.

fc is an instance of a class that wraps my application's bound Control objects. It has a Control property (the wrapped control, obviously), and then Binding and ErrorProvider properties whose use is shown below. Note that since this code sets up the data binding for you, you don't set up the control's binding in the form designer. You don't, strictly speaking, need this class in order to make this code work, but it does simplify the code somewhat. (All of the code here is from a static method on the fc class to which the BindingSource has been passed, as shown.)

cm is an instance of a class that contains metainformation about the data column that I'm binding the control to, notably:

  • ColumnName, its name in the data source (I'm obviously binding to a DataColumn),
  • PropertyName, the name of the control property it's bound to (e.g. "Text"),
  • NullValue, as described in the documentation for Binding.NullValue,
  • Format, the method that formats the column's internal value for display in the bound control, and
  • Parse, the method for parsing input into the column's internal value. The actual validation logic for a column lives here.

This is C#, obviously, so you'll need to mess with it in order to get it to work in VB, but the differences should simply be matters of syntax.

 // add an ErrorProvider to the control so that we have a place to display
 // error messages
 fc.ErrorProvider = new ErrorProvider {BlinkStyle = ErrorBlinkStyle.NeverBlink};

 // create the Binding.  DataSourceUpdateMode.Never bypasses automatic updating
 // of the data source; data only gets written to the data source when the
 // column is successfully validated.
 fc.Binding = fc.Control.DataBindings.Add(
    cm.PropertyName,
    bindingSource,
    cm.ColumnName,
    true,
    DataSourceUpdateMode.Never,
    cm.NullValue);

 // this is called whenever the Binding pushes data back to the data source;
 // it parses the data in the control into an object that's returned in e.Value.
 fc.Binding.Parse += delegate(object sender, ConvertEventArgs e)
 {
     string property = fc.Binding.PropertyName;
     object unparsedValue = fc.Control.GetType().GetProperty(property).GetValue(fc.Control, null);

     string message;
     // note that we don't actually care about the parsed value if message
     // is not null (i.e. if the value is invalid).  by convention it's null,
     // but it won't ever get written back to the data source.
     object parsedValue = cm.Parse(unparsedValue, out message);
     if (message != null)
     {
         fc.ErrorProvider.SetError(fc.Control, message);
     }
     else
     {
         fc.ErrorProvider.Clear();
     } 
     e.Value = parsedValue ?? DBNull.Value;
  };

  // this is called whenever the user leaves the Control.
  fc.Control.Validating += delegate
                                     {
     string property = fc.Binding.PropertyName;
     object value = fc.Control.GetType().GetProperty(property).GetValue(fc.Control, null);
     string message;
     cm.Parse(value, out message);
     if (message != null)
     {
         fc.ErrorProvider.SetError(fc.Control, message);
     }
     else
     {
         fc.ErrorProvider.Clear();
     }
 };

 // this, combined with the DataSourceUpdateMode of Never, insures that the Control's
 // value only gets pushed out to the data source after validation is successful.
 fc.Control.Validated += delegate {
     fc.Binding.WriteValue();
 };

 // this is called whenever the Binding pulls data from the data source into 
 // the bound Control
 fc.Binding.Format += delegate(object sender, ConvertEventArgs e)
 {
     e.Value = cm.Format(e.Value);
 };
Robert Rossney