Thanks to Michael for his help. Unfortunately, I need bindings to remain constantly synchronized, with only validation deferred. Otherwise, Michael's solution would do the job. So, I went in a slightly different direction. Here is the solution I ultimately implemented.
Simple demo: Let's start with the simplest case: My business objects implement IDataErrorInfo
; let's assume I can modify how it is implemented. I give each business object an additional boolean property, ValidationEnabled
, and I modify the IDataErrorInfo
implementation to always return a null result if that property is false.
The demo I mentioned in my original post uses a Contact object with two properties; FirstName and LastName. I added a ValidationEnabled property and modified the IDataErrorInfo
implementation looks like this:
#region IDataErrorInfo Members
public string Error
{
get { throw new System.NotImplementedException(); }
}
public string this[string columnName]
{
get
{
// Initialize
string result = null;
// Perform validation only if enabled
if (ValidationEnabled)
{
switch (columnName)
{
// Validate 'First Name'
case "FirstName":
if (string.IsNullOrEmpty(FirstName))
{
result = "First name has to be set";
}
else if(FirstName.Length < 5)
{
result = "First name must be at least five characters";
}
break;
// Validate "Last Name"
case "LastName":
if (string.IsNullOrEmpty(LastName))
{
result = "Last name has to be set";
}
else if (LastName.Length < 5)
{
result = "Last name must be at least five characters";
}
break;
}
}
// Set return value
return result;
}
}
#endregion
In my version of the simple demo, I connect the Submit button to an event handler in code-behind that enables validation and refreshes the bindings for both text boxes:
private void OnButtonClick(object sender, RoutedEventArgs e)
{
var contact = (Contact) DataContext;
contact.ValidationEnabled = true;
var binding = FirstNameBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
binding = LastNameBox.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
}
Now the red outlines don't appear until I try to submit the form.
Real world: In my production app, I don't control the business objects, and I can't modify them as I did in my simple demo. So, I create a simple pass-through wrapper for the business object that exposes the properties to be databound to the view and links those properties to the corresponding properties of the wrapped business object. The wrapper implements IDataErrorInfo and contains the ValidationEnabled
property.
The wrapper's IDataErrorInfo implementation always returns null if validation is not enabled. If validation is enabled, then the wrapper calls the IDataErrorInfo implementation on the wrapped object and returns what it gets from there.
This approach will be familiar to anyone who uses the Model-View-ViewModel pattern. What we are doing is creating a view-model wrapper for a business object, which is considered an MVVM best practice by many developers. It separates the UI concern (supressing red borders until the page is submitted) from the business model concern (simple object validation).
Also, my production app will not use event handlers in code-behind. Per MVVM, the button will be wired to an ICommand, which will contain the logic from the OnButtonClick() handler in the simple demo.
I hope that's helpful to anyone else researching this issue down the road.