Donnerstag, 17. Februar 2011

ValidationRule with Parameter using AttatchedProperty

Usually it's a good idea to use Converters and Valitators to handle the data exchange between user interface and data source.

But ValitationRules have a big disadvantage...there is no easy way to get some parameters inside. Well, static parameters aren't a problem, but I had to use Bindings for them. There are several work-arounds in the internet, like this one:

http://khason.net/blog/fully-binded-validation-by-using-dependency-and-attached-properties/

My problem is, that my parameters aren't DependencyProperties...they rely on a DataTable which is set to DataContextProperty.

Enogh talking, here's my solution:

First of all I have a ValidationRule like this:

  public class DateInRangeValidator : ValidationRule
  {
    public DateTime Min { get; set; }
    public DateTime Max { get; set; }

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
      if (value != null
        && value is DateTime
        && (this.Min.CompareTo((DateTime)value) > 0
        | this.Max.CompareTo((DateTime)value) < 0))
      {
        return new ValidationResult(false, "Wrong Date!");
      }
      return new ValidationResult(true, "Everything is fine.");
    }
  }



With fix values for minimum and maximun a sample XAML-code looks like this:

  <Grid Name="grid1">
    <DatePicker Name="datepicker">
      <DatePicker.SelectedDate>
        <Binding Path="column1" UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
            <util:DateInRangeValidator Min="1.1.2008" Max="1.1.2020"/>
          </Binding.ValidationRules>
        </Binding>
      </DatePicker.SelectedDate>
    </DatePicker>
  </Grid>



with this code-behind:

  private void Window_Initialized(object sender, EventArgs e)
  {
    DataTable datatable = new DataTable
    {
      Columns =
      {
        new DataColumn("column1", typeof(DateTime)),
        new DataColumn("column2", typeof(DateTime)),
        new DataColumn("column3", typeof(DateTime))
      }
    };

    DataRow datarow = datatable.NewRow();
    datarow["column1"] = new DateTime(2010, 1, 1);
    datarow["column2"] = new DateTime(2005, 1, 1);
    datarow["column3"] = new DateTime(2020, 1, 1);
    datatable.Rows.Add(datarow);

    this.grid1.DataContext = datatable;
  }


But now we'd like to use Bindings to set the DateInRangeValidator.Min and DateInRangeValidator.Max values. Maybe the are used elsewhere and aren't static during runtime.

There are some approches like using an embedded DependencyObject in the ValidationRule, but those aren't part of the logical tree and cannot use DataContext (just static ressources). 


Also there are some approches like this one: http://michlg.wordpress.com/2010/01/29/wpf-custom-validationrule-with-an-additional-parameter/ using a work-around to simulate input and output-parameters.

Well...my idea is based on the first link on top of this post. I define a class based on DependencyObject:

  public class MinMaxRange : DependencyObject

Inside I define two attatched DependencyProperties: one for minimum:

  public static object GetMin(DependencyObject obj)
  {
    return (object)obj.GetValue(MinProperty);
  }
  public static void SetMin(DependencyObject obj, int value)
  {
    obj.SetValue(MinProperty, value);
  }
  public static readonly DependencyProperty MinProperty
    = DependencyProperty.RegisterAttached("Min",
        typeof(object),
        typeof(MinMaxRange),
        new UIPropertyMetadata(null, OnAttachedPropertyChanged));



and one for maximum:

  public static object GetMax(DependencyObject obj)
  {
    return (object)obj.GetValue(MaxProperty);
  }
  public static void SetMax(DependencyObject obj, int value)
  {
    obj.SetValue(MaxProperty, value);
  }
  public static readonly DependencyProperty MaxProperty
    = DependencyProperty.RegisterAttached("Max",
        typeof(object),
        typeof(MinMaxRange),
        new UIPropertyMetadata(null, OnAttachedPropertyChanged));



The OnAttatchedPropertyChanged-Event is the heart of this solution and will be discussed later. First I'd like to discribe my target: When you have this XAML-code:

  <Grid Name="grid1">
    <DatePicker Name="datepicker"
                util:MinMaxRange.Min="{Binding column2}"
                util:MinMaxRange.Max="{Binding column3}">
      <DatePicker.SelectedDate>
        <Binding Path="column1" UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
            <util:DateInRangeValidator/>
          </Binding.ValidationRules>
        </Binding>
      </DatePicker.SelectedDate>
    </DatePicker>
  </Grid>

the code-behind should look for each DependencyProperty with Binding. Each Binding with ValidationRule which has a Min- and Max-property should get the value of the attatched property.

Here is the code of OnAttatchedPropertyChanged (with some comments):

  static void OnAttachedPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
  {
    // array of all fields which allow bindings:
    System.Reflection.FieldInfo[] properties
      = obj.GetType().GetFields(
            System.Reflection.BindingFlags.Public
          | System.Reflection.BindingFlags.Static
          | System.Reflection.BindingFlags.FlattenHierarchy);
    // filter array for DependencyProperties:
    List<DependencyProperty> dependencypropertieslist = new List<DependencyProperty>();
    foreach (System.Reflection.FieldInfo fieldinfo in properties)
      if (fieldinfo.FieldType == typeof(DependencyProperty))
        dependencypropertieslist.Add((DependencyProperty)fieldinfo.GetValue(null));
    // run through all DependencyProperties:
    foreach (DependencyProperty dp in dependencypropertieslist)
      // has DependencyProperty Binding?
      if (BindingOperations.IsDataBound(obj, dp))
      {
        // cast Binding:
        Binding binding = BindingOperations.GetBinding(obj, dp);
        if (binding != null)
        {
          // loop over all ValidationRules:
          foreach (ValidationRule validationrule in binding.ValidationRules)
          {
            // loop over all properties:
            foreach (System.Reflection.PropertyInfo pi in validationrule.GetType().GetProperties())
              if (pi.Name.Equals(e.Property.Name))
              {
                // assign value:
                pi.SetValue(validationrule, e.NewValue, null);
              }
          }
        }
      }
  }

Basicly when the attatched property Min or Max changes, this method looks for all ValidationRules with property Min or Max.

Please pay attention, that there is no safe casting of the value-type. Also every ValidationRule with those parameters will be set.



Have fun! And if there are any mistakes in this, let me know :-) 

Keine Kommentare:

Kommentar veröffentlichen