Improved IValueConverter — MarkupExtension or Depe

2019-01-22 09:18发布

问题:

I saw online 2 different approaches to enhancing an IValueConverter. One of them extended a ValueConverter from MarkupExtension, the other from DependencyObject. I can't extend from both, so I'm wondering if any one is better than the other?

回答1:

Deriving from each gives you different kind of power and flexibility:

  • Deriving from MarkupExtension enables you to use the value converter without making it a static resource, as described below:

    public class DoubleMe : MarkupExtension, IValueConverter
    {
       public override object ProvideValue(IServiceProvider serviceProvider)
       {
          return this;
       }
       public object Convert(object value, /*rest of parameters*/ )
       {
          if ( value is int )
             return (int)(value) * 2; //double it
          else
             return value.ToString() + value.ToString();
       }
      //...
    }
    

    In XAML, you can directly use it without creating a StaticResource:

    <TextBlock Text="{Binding Name, Converter={local:DoubleMe}}"/>
    <TextBlock Text="{Binding Age, Converter={local:DoubleMe}}"/>
    

    Such code is very handy when debugging, as you can just write local:DebugMe and then can debug the DataContext of the control on which you use it.

  • Deriving from DependencyObject enables you to configure the value converter with some preferences in a more expressive way, as described below:

    public class TruncateMe : DependencyObject, IValueConverter
    {
         public static readonly DependencyProperty MaxLengthProperty =
             DependencyProperty.Register("MaxLength",
                                          typeof(int),
                                          typeof(TruncateMe),
                                          new PropertyMetadata(100));
         public int MaxLength
         {
             get { return (int) this.GetValue(MaxLengthProperty); }
             set { this.SetValue(MaxLengthProperty, value); }
         }
    
         public object Convert(object value, /*rest of parameters*/ )
         {
            string s = value.ToString();
            if ( s.Length > MaxLength)
              return s.Substring(0, MaxLength) + "...";
          else
              return s;
         }
         //...
    }
    

    In XAML, you can directly use it as:

    <TextBlock>
       <TextBlock.Text>
           <Binding Path="FullDescription">
               <Binding.Converter>
                 <local:TruncateMe MaxLength="50"/>
               </Binding.Converter>
           </Binding>
       </TextBlock.Text> 
    

    What does it do? It truncates the string FullDescription if it is more than 50 characters!

@crazyarabian commented that:

Your statement "Deriving from DependencyObject enables you to configure the value converter with some preferences in a more expressive way" isn't exclusive to DependencyObject as you can create the same MaxLength property on a MarkupExtension resulting in <TextBlock Text="Binding Age, Converter={local:DoubleMe, MaxLength=50}}"/>. I would argue that a MarkupExtension is more expressive and less verbose.

That is true. But then that is not bindable; that is, when you derive from MarkupExtension, then you cannot do :

MaxLength="{Binding TextLength}"

But if you derive your converter from DependencyObject, then you can do the above. In that sense, it is more expressive compared to MarkupExtension.

Note that the target property must be a DependencyProperty for Binding to work. MSDN says,

  • Each binding typically has these four components: a binding target object, a target property, a binding source, and a Path to the value in the binding source to use. For example, if you want to bind the content of a TextBox to the Name property of an Employee object, your target object is the TextBox, the target property is the Text property, the value to use is Name, and the source object is the Employee object.

  • The target property must be a dependency property.



回答2:

Since it's my library you're citing as an example of converters that extend DependencyObject, I think it fitting to explain myself.

I actually started out by simply implementing IValueConverter with Object as my base class. The only reason I switched to extending DependencyObject was to allow for a technique - pioneered by Josh Smith - called virtual branching. You can read about that technique here.

Suppose you want to do something like this:

<UserControl.Resources>
    <con:CaseConverter Casing="{Binding SomeProperty}"/>
</UserControl.Resources>

This won't work because resources are not part of the visual tree, and so the binding will fail. Virtual branching hacks around this little dilemma, enabling you to perform such a binding. However, it still relies - like any other WPF binding - on the target being a DependencyObject. Thus, if I simply implemented IValueConverter without extending DependencyObject, you would be precluded from using virtual branches.

Now, if I'm completely honest, I'm not sure I'd still do this if I had my time again. I've never actually had to use a virtual branch myself - I just wanted to enable the scenario. I may even change this in a future version of my library. So my advice would be stick to a base class of Object (or a simple derivative thereof) unless you really think you'll need virtual branching.