Enforcing Complex Business Data Rules with WPF

Brian Noyes

Download the Code Sample

Microsoft Windows Presentation Foundation (WPF) has a rich data-binding system. In addition to being a key enabler for loose coupling of the UI definition from the supporting logic and data through the Model-View-ViewModel (MVVM) pattern, the data-binding system has powerful and flexible support for business data-validation scenarios. The data-binding mechanisms in WPF include several options for evaluating the validity of input data when you create an editable view. Plus, WPF templating and styling capabilities for controls give you the ability to easily customize the way you indicate validation errors to the user.

To support complex rules and to display validation errors to the user, you generally need to employ a combination of the available validation mechanisms. Even a seemingly simple data input form can present validation challenges when the business rules get complex. Common scenarios involve both simple rules at an individual property level, and cross-coupled properties where the validity of one property depends on the value of another property. However, the validation support in WPF data binding makes it easy to address these challenges.

In this article, you’ll see how to use the IDataErrorInfo interface implementation, ValidationRules, BindingGroups, exceptions, and validation-related attached properties and events to address your data-validation needs. You’ll also see how to customize the display of validation errors with your own ErrorTemplates and ToolTips. For this article, I assume you are already familiar with the basic data-binding capabilities of WPF. For more background on that, see John Papa’s December 2007 MSDN Magazine article, “Data Binding in WPF”.

Data Validation Overview

Almost any time you enter or modify data in an application, you need to ensure that the data is valid before it gets too far away from the source of those changes—in this case, the user. Moreover, you need to give users a clear indication when the data they entered is invalid, and hopefully also give them some indication of how to correct it. These things are fairly easy to do with WPF as long as you know which capability to use and when.

When you use data binding in WPF to present business data, you typically use a Binding object to provide a data pipeline between a single property on a target control and a data source object property. For validation to be relevant, you’re typically doing TwoWay data binding—meaning that, in addition to data flowing from the source property into the target property for display, the edited data also flows from target to source as shown in Figure 1.

Figure 1 Data Flow in TwoWay Data Binding
Figure 1 Data Flow in TwoWay Data Binding
There are three mechanisms for determining whether data entered through a data-bound control is valid. These are summarized in Figure 2.

Figure 2 Binding Validation Mechanisms

Validation Mechanism Description
Exceptions By setting the ValidatesOnExceptions property on a Binding object, if an exception is raised in the process of trying to set the modified value on the source object property, a validation error will be set for that Binding.
ValidationRules The Binding class has a property to supply a collection of ValidationRule-derived class instances. These ValidationRules need to override a Validate method that will be called by the Binding whenever the data in the bound control changes. If the Validate method returns an invalid ValidationResult object, a validation error is set for that Binding.
IDataErrorInfo By implementing the IDataErrorInfo interface on a bound data-source object and setting the ValidatesOnDataErrors property on a Binding object, the Binding will make calls to the IDataErrorInfo API exposed from the bound data-source object. If non-null or non-empty strings are returned from those property calls, a validation error is set for that Binding.

When a user enters or modifies data in TwoWay data binding, a workflow kicks off:

  • Data is entered or modified by the user through keystrokes, mouse, touch, or pen interaction with the element, resulting in a change of a property on the element.
  • Data is converted to the data-source property type, if needed.
  • The source property value is set.
  • The Binding.SourceUpdated attached event fires.
  • Exceptions are caught by the Binding if thrown by the setter on the data-source property, and can be used to indicate a validation error.
  • IDataErrorInfo properties are called on the data source object, if implemented.
  • Validation error indications are presented to the user and the Validation.Error attached event fires.

As you can see, there are several points in the process where validation errors can result, depending on which mechanism you choose. Not shown in the list is where the ValidationRules fire. That’s because they can fire at various points in the process, depending on the value you set for the ValidationStep property on the ValidationRule, including before type conversion, after conversion, after the property is updated or when the changed value is committed (if the data object implements IEditableObject). The default value is RawProposedValue, which happens before type conversion. The point when the data is converted from the target control property type to the data source object property type usually happens implicitly without touching any of your code, such as for a numeric input in a TextBox. This type-conversion process can throw exceptions, which should be used to indicate a validation error to the user.

If the value can’t even be written to the source object property, clearly it is invalid input. If you choose to hook up ValidationRules, they are invoked at the point in the process indicated by the ValidationStep property, and they can return validation errors based on whatever logic is embedded in them or called from them. If the source object property setter throws an exception, that should almost always be treated as a validation error, as with the type conversion case.

Finally, if you implement IDataErrorInfo, the indexer property you add to your data source object for that interface will be called for the property that was being set to see if there is a validation error based on the returned string from that interface. I’ll cover each of these mechanisms in more detail a bit later.

When you want validation to occur is another decision you’ll have to make. Validation happens when the Binding writes the data to the underlying source object property. When validation takes place is specified by the UpdateSourceTrigger property of the Binding, which is set to PropertyChanged for most properties. Some properties, such as TextBox.Text, change the value to FocusChange, which means that validation happens when the focus leaves the control that’s being used to edit data. The value can also be set to Explicit, which means that validation has to be explicitly invoked on the binding. The BindingGroup that I discuss later in the article uses Explicit mode.

In validation scenarios, particularly with TextBoxes, you typically want to give fairly immediate feedback to the user. To support that, you should set the UpdateSourceTrigger property on the Binding to PropertyChanged:

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

It turns out that for many real validation scenarios, you’ll need to leverage more than one of these mechanisms. Each has its pros and cons, based on the kind of validation error you’re concerned with and where the validation logic can reside.

Business Validation Scenario

To make this more concrete, let’s walk through an editing scenario with a semi-real business context and you’ll see how each of these mechanisms can come into play. This scenario and the validation rules are based on a real application I wrote for a customer in which a fairly simple form required the use of almost every validation mechanism due to the supporting business rules for validation. For the simpler application used in this article, I’ll employ each of the mechanisms to demonstrate their use, even though they’re not all explicitly required.

Let’s suppose you need to write an application to support field technicians who perform in-home customer support calls (think the cable guy, but one who also tries to up-sell additional features and services). For each activity the technician performs in the field, he needs to enter an activity report that tells what he did and relates it to several pieces of data. The object model is shown in Figure 3.

Figure 3 Object Model for the Sample Application
Figure 3 Object Model for the Sample Application

The main piece of data users fill out is an Activity object, including a Title, the ActivityDate, an ActivityType (a drop-down selection of predefined activity types) and a Description. They also need to relate their activity to one of three possibilities. They need to select either a Customer the activity was performed for from a list of customers assigned to them or an Objective of the company the activity was related to from a list of company objectives, or they can manually enter a Reason if neither a Customer nor an Objective apply for this activity.

Here are the validation rules the application needs to enforce:

  • Title and Description are required fields.
  • The ActivityDate must be no earlier than seven days prior to the current date and no later than seven days in the future.
  • If the ActivityType Installis selected, the Inventory field is required and should indicate the pieces of equipment from the technician’s truck that were expended. The inventory items need to be entered as a comma-separated list with an expected model number structure for the input items.
  • At least one Customer, Objective or Reason must be provided.

These may seem like fairly simple requirements, but the last two in particular are not so straightforward to address because they indicate cross-coupling between properties. The running application with some invalid data—indicated by the red box—is shown in Figure 4.

Figure 4 A Dialog Showing ToolTips and Invalid Data
Figure 4 A Dialog Showing ToolTips and Invalid Data

Exception Validation

The simplest form of validation is to have an exception that’s raised in the process of setting the target property treated as a validation error. The exception could result from the type conversion process before the Binding ever sets the target property; it could result from an explicit throw of an exception in the property setter; or it could result from a call out to a business object from the setter where the exception gets thrown further down the stack.

To use this mechanism, you simply set the ValidatesOnExceptions property to true on your Binding object:

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

When an exception is thrown while trying to set the source object property (Activity.Title in this case), a validation error will be set on the control. The default validation error indication is a red border around the control as shown in Figure 5.

Figure 5 A Validation Error
Figure 5 A Validation Error

Because exceptions can occur in the type conversion process, it’s a good idea to set this property on input Bindings whenever there’s any chance of the type conversion failing, even if the backing property just sets the value on a member variable with no chance of an exception.

For example, suppose you were to use a TextBox as the input control for a DateTime property. If a user enters a string that can’t be converted, ValidatesOnExceptions is the only way your Binding could indicate an error, because the source object property will never be called.

If you need to do something specific in the view when there is invalid data, such as disable a command, you can hook the Validation.Error attached event on the control. You’ll also need to set the NotifyOnValidationError property to true on the Binding.

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

ValidationRule Validation

In some scenarios, you might want to tie the validation in at the UI level and need more complicated logic to determine whether the input is valid. For the sample application, consider the validation rule for the Inventory field. If data is entered, it needs to be a comma-separated list of model numbers that follow a specific pattern. A ValidationRule can easily accommodate this because it depends entirely on the value being set. The ValidationRule can use a string.Split call to turn the input into a string array, then use a regular expression to check whether the individual parts comply with a given pattern. To do this, you can define a ValidationRule as shown in Figure 6.

Figure 6 ValidationRule to Validate a String Array

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

Properties exposed on a ValidationRule can be set from the XAML at the point of use, allowing them to be a little more flexible. This validation rule ignores values that can’t be converted to a string array. But when the rule can execute the string.Split, it then uses a RegEx to validate that each string in the comma-separated list complies with the pattern set through the InventoryPattern property.

When you return a ValidationResult with the valid flag set to false, the error message you provide can be used in the UI to present the error to the user, as I’ll show later. One downside to ValidationRules is that you need an expanded Binding element in the XAML to hook it up, as shown in the following code:

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

In this example, my Binding will still raise validation errors if an exception occurs due to the ValidatesOnExceptions property being set to true, and I also support IDataErrorInfo validation based on the ValidatesOnDataErrors being set to true, which I’ll talk about next.

If you have multiple ValidationRules attached to the same property, those rules can each have different values for the ValidationStep property or they can have the same value. Rules within the same ValidationStep are evaluated in order of declaration. Rules in earlier ValidationSteps obviously run before those in later ValidationSteps. What may not be obvious is that if a ValidationRule returns an error, none of the subsequent rules are evaluated. So the first validation error will be the only one indicated when the errors result from ValidationRules.

IDataErrorInfo Validation

The IDataErrorInfo interface requires the implementer to expose one property and one indexer:

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}

The Error property is used to indicate an error for the object as a whole, and the indexer is used to indicate errors at the individual property level. They both work the same: returning a non-null or non-empty string indicates a validation error. In addition, the string you return can be used to display the error to the user, as I’ll show later.

When you’re working with individual controls bound to individual properties on a data source object, the most important part of the interface is the indexer. The Error property is used only in scenarios such as when the object is displayed in a DataGrid or in a BindingGroup. The Error property is used to indicate an error at the row level, whereas the indexer is used to indicate an error at the cell level.

Implementing IDataErrorInfo has one big downside: the implementation of the indexer typically leads to a big switch-case statement, with one case for each property name in the object, and you have to switch and match based on strings and return strings to indicate an error. Furthermore, your implementation of IDataErrorInfo is not called until the property value has already been set on the object. If other objects have subscribed to INotifyPropertyChanged.PropertyChanged on your object, they will already have been notified of the change and could have started working based on data that your IDataErrorInfo implementation is about to declare invalid. If that could be a problem for your application, you’ll need to throw exceptions from the property setters when you’re unhappy with the value being set.

The good thing about IDataErrorInfo is that it makes it easy to address cross-coupled properties. For example, in addition to using the ValidationRule to validate the input format of the Inventory field, remember the requirement that the Inventory field must be filled in when the ActivityType is Install. The ValidationRule itself has no access to the other properties on the data-bound object. It just gets passed a value that’s being set for the property the Binding is hooked up to. To address this requirement, when the ActivityType property gets set you need to cause validation to occur on the Inventory property and return an invalid result when ActivityType is set to Install if the value of Inventory is empty.

To accomplish this, you need IDataErrorInfo so that you can inspect both the Inventory and ActivityType properties when evaluating Inventory, as shown here:

public string this[string propertyName] {
  get { return IsValid(propertyName); }
}

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

Additionally, you need to get the Inventory Binding to invoke validation when the ActivityType property changes. Normally, a Binding only queries the IDataErrorInfo implementation or calls ValidationRules if that property changed in the UI. In this case, I want to trigger the re-evaluation of the Binding validation even though the Inventory property has not changed, but the related ActivityType has.

There are two ways to get the Inventory Binding to refresh when the ActivityType property changes. The first and simplest way is to publish the PropertyChanged event for Inventory when you set the ActivityType:

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

This causes the Binding to refresh and re-evaluate the validation of that Binding.

The second way is to hook the Binding.SourceUpdated attached event on the ActivityType ComboBox or one of its parent elements, and trigger a Binding refresh from the code-behind handler for that event:

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

Calling UpdateSource on a Binding programmatically causes it to write the current value in the bound target element into the source property, triggering the validation chain as if the user had just edited the control.

Using BindingGroup for Cross-Coupled Properties

The BindingGroup feature was added in the Microsoft .NET Framework 3.5 SP1. A BindingGroup is specifically designed to allow you to evaluate validation on a group of bindings all at once. For example, you could allow a user to fill in an entire form and wait until she pressed the Submit or Save button to evaluate the validation rules for the form, then present the validation errors all at once. In the sample application, I had the requirement that at least one Customer, Objective, or Reason had to be provided. A BindingGroup can be used to evaluate a subset of a form as well.

To use a BindingGroup, you need a set of controls with normal Bindings on them that share a common ancestor element. In the sample application, the Customer ComboBox, Objective ComboBox and Reason TextBox all live within the same Grid for layout. BindingGroup is a property on FrameworkElement. It has a ValidationRules collection property that you can populate with one or more ValidationRule objects. The following XAML shows the BindingGroup hookup for the sample application:

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

In this example, I added an instance of the CustomerObjectiveOrReasonValidationRule to the collection. The ValidationStep property allows you to have some control over the value that’s passed to the rule. UpdatedValue means to use the value that was written to the data source object after it is written. You can also choose values for ValidationStep that let you use the raw input from the user, the value after type and value conversion is applied, or the “committed” value, which means implementing the IEditableObject interface for transactional changes to the properties of your object.

The ValidatesOnTargetUpdated flag causes the rule to be evaluated each time the target property is set through the Bindings. This includes when it is set initially, so you have immediate validation error indications if the initial data is invalid, as well as each time the user changes the values in the controls that are part of the BindingGroup.

A ValidationRule that is hooked up to a BindingGroup works a little differently than a ValidationRule hooked up to a single Binding. Figure 7 shows the ValidationRule hooked up to the BindingGroup shown in the previous code sample.

Figure 7 ValidationRule for a BindingGroup

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

In a ValidationRule hooked up to a single Binding, the value that’s passed in is the single value from the data source property that’s set as the Path of the Binding. In the case of a BindingGroup, the value that is passed to the ValidationRule is the BindingGroup itself. It contains an Items collection that is populated by the DataContext of the containing element, in this case the Grid.

For the sample application, I’m using the MVVM pattern, so the DataContext of the view is the ViewModel itself. The Items collection contains just a single reference to the ViewModel. From the ViewModel, I can get to the Activity property on it. The Activity class in this case has the validation method that determines whether at least one Customer, Objective, or Reason has been entered so I don’t have to duplicate that logic in the ValidationRule.

As with other ValidationRules covered earlier, if you’re happy with the values of the data passed in, you return a ValidationResult.ValidResult. If you’re unhappy, you construct a new ValidationResult with a false valid flag and a string message indicating the problem, which can then be used for display purposes.

Setting the ValidatesOnTargetUpdated flag is not enough to get the ValidationRules to fire automatically, though. The BindingGroup was designed around the concept of explicitly triggering validation for an entire group of controls, typically through something like a Submit or Save button press on a form. In some scenarios, users don’t want to be bothered with validation error indications until they consider the editing process complete, so the BindingGroup is designed with this approach in mind.

In the sample application, I want to provide immediate validation-error feedback to the user any time he changes something in the form. To do that with a BindingGroup, you have to hook the appropriate change event on the individual input controls that are part of the group, and have the event handler for those events trigger the evaluation of the BindingGroup. In the sample application, this means hooking the ComboBox.SelectionChanged event on the two ComboBoxes and the TextBox.TextChanged event on the TextBox. Those all can point to a single handling method in the code-behind:

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

Note that for the validation display, the default red border will be displayed on the FrameworkElement that the BindingGroup resides on, such as the Grid in the sample application, as in Figure 4. You can also alter where the validation indication is displayed by using the Validation.ValidationAdornerSite and Validation.ValidationAdornerSiteFor attached properties. By default, the individual controls will also display red borders for their individual validation errors. In the sample application, I turn those borders off by setting the ErrorTemplate to null through Styles.

With BindingGroup in the .NET Framework 3.5 SP1, you may encounter problems with the proper display of validation errors on initial form load, even if you set the ValidatesOnTargetUpdated property on the ValidationRule. A workaround I found for this was to “jiggle” one of the bound properties in the BindingGroup. In the sample application, you could add and remove a space at the end of whatever text is initially presented in the TextBox in the Loaded event of the view like so:

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

This causes the BindingGroup ValidationRules to fire since one of the contained Binding properties has changed, causing the validation of each Binding to be called. This behavior is fixed in the .NET Framework 4.0, so there should be no need for the workaround to get initial display of validation errors—just set the ValidatesOnTargetUpdated property to true on the validation rules.

Validation Error Display

As mentioned previously, the default way WPF displays validation errors is to draw a red border around the control. Often you’ll want to customize this to display errors in some other way. Moreover, the error message associated with the validation error is not displayed by default. A common requirement is to display the error message in a ToolTip only when the validation error exists. Customizing the validation error displays is fairly easy through a combination of Styles and a set of attached properties associated with validation.

To add a ToolTip that displays the error text is trivial. You just need to define a Style that applies to the input control that sets the ToolTip property on the control to the validation error text whenever there is a validation error. To support this, there are two attached properties you’ll need to employ: Validation.HasError and Validation.Errors. A Style targeting the TextBox type that sets the ToolTip is shown here:

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>

You can see that the Style just contains a property trigger for the Validation.HasError attached property. The HasError property will be set to true when a Binding updates its source object property and the validation mechanisms generate an error. That could come from an exception, ValidationRule or IDataErrorInfo call. The Style then uses the Validation.Errors attached property, which will contain a collection of error strings if a validation error exists. You can use the CurrentItem property on that collection type to just grab the first string in the collection. Or you could design something that data binds to the collection and displays the ErrorContent property for each item in a list-oriented control.

To change the default validation error display for a control to something other than the red border, you will need to set the Validation.ErrorTemplate attached property to a new template on the control you want to customize. In the sample application, instead of displaying a red border, a small red gradient circle is displayed to the right of each control with an error. To do that, you define a control template that will be used as the ErrorTemplate.

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

To hook up that control template to a control, you just need to set the Validation.ErrorTemplate property for the control, which you can again do through a Style:

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>

Wrap Up

In this article, I’ve shown how you can use the three validation mechanisms of WPF data binding to address a number of business data validation scenarios. You saw how to use exceptions, ValidationRules, and the IDataErrorInfo interface to address single property validation, as well as properties whose validation rules depend on the current values of other properties on the control. You also saw how to use BindingGroups to evaluate several Bindings at once, and how to customize the display of errors beyond the defaults of WPF.

The sample application for this article has the full set of validation that satisfies the described business rules in a simple application that uses MVVM to hook up the view to the data supporting it.


Brian Noyes  is chief architect of IDesign (idesign.net), a Microsoft regional director and Microsoft MVP. Noyes is an author and a frequent speaker at Microsoft Tech·Ed, DevConnections, DevTeach and other conferences worldwide. Contact him through his blog at briannoyes.net

Thanks to the following technical expert for reviewing this article:  Sam Bent

This entry was posted in UI. Bookmark the permalink.

1 条 Enforcing Complex Business Data Rules with WPF 的回复

  1. ass@nigger.com说道:

    rofl who is the unit inch goat humper who posted this worthless crap article
    brain noyes rofl mean sucks the nigger dick in any language

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s