简体   繁体   中英

How can I use an IValueConverter to bind to different properties of an object in WPF?

I have a property in my view model which has a property of a class which has multiple properties eg

public class Content
{
    public int Selector { get; set; }
    public int Value1 { get; set; }
    public int Value2 { get; set; }
    public int Value3 { get; set; }
    public int Value4 { get; set; }
}

public class ViewModel
{
    public Content ContentInstance { get; set; }
}

and I want to bind it in my xaml with a converter such that the value of the selector determines which value is bound to the element eg

<TextBox Text="{Binding ContentInstance, Converter="ContentValueConverter", TargetNullValue='', Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

So far I have:

public class ContentValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)    
    {
        var contentPart = value as Content;
        if(contentPart == null) return;

        switch(contentPart.Selector)
        {
            case 1:
                return contentPart.Value1;
            //etc
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

This works to display the value but does not save the value back to the model. I would prefer to keep this in a IValueConverter as this has to be added to many places in the codebase. Any help to get the value saving back to the model would be appreciated.

Bind a textbox to each property and put them in the same grid column/row. Bind their visibility to the selector property with a converter that takes which value of the selector will make it visible. Now your visible textbox is bound to the right property and controlled by your selector selection. You can make it reusable by putting it in a separate UserControl or a ControlTemplate.

You need to implement also the ConvertBack method on ContentValueConverter. ConvertBack is used to convert the result back to the view model

You can't do it like this because you need a ConvertBack method that returns a new Content instance.

I would create 5 text boxes, each binded two way on the field and hide them based on the Selector value (later edit: I see Lee O. gave the same solution but here is some code).

<TextBox Text="{Binding ContentInstance.Value1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
         Visibility="{Binding ContentInstance.Selector, Converter=SelectorToVisibilityConverter, ConverterParameter=1}"/>
<TextBox Text="{Binding ContentInstance.Value2, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
         Visibility="{Binding ContentInstance.Selector, Converter=SelectorToVisibilityConverter, ConverterParameter=2}"/>
...

and the converter:

public class SelectorToVisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)    
    {
        var selector = (int)value;
        var desired = (int)parameter; //may need a string to int conversion

        return selector == desired ? Visibility.Visible : Visibility.Collapsed;
    }
}

Note that this is written in notepad and it's not tested at all!

I guess you could also use some attached behavior.

Your approach has one more flaw - any changes made to any of Content 's properties will not be picked up by WPF, even if Content implements INotifyPropertyChanged .

If you don't care then, theoretically, depending on your scenario, you could store reference to the Content object that gets passed to Convert method and reuse it in ConvertBack . It's not very clean nor WPFish, requires a separate converter's instance per binding (so converter has to be defined inline, not as a resource).

So why don't you implement a proxy property in your ViewModel instead?

public class ViewModel
{
    public Content ContentInstance { get; set; }
    public int Value
    {
        get
        {
            switch (Content.Selector)
            {
                case 1:
                    return contentPart.Value1;
                //etc
            }
        }
        set
        {
            switch (Content.Selector)
            {
                case 1:
                    contentPart.Value1 = value;
                    break;
                //etc
            }
        }
    }
}

Then you can bind directly to it:

<TextBox Text="{Binding Value, Mode=TwoWay}"/>

Clean and effective. If your Content implements INotifyPropertyChanged then ViewModel can intercept it and raise changed events for Value property too.

Here is a solution based on an attached dependency property:

public static class GetValueBasedOnContentSelectorBehavior
{
    private static readonly Dictionary<Content, TextBox> Map = new Dictionary<Content, TextBox>();

    public static readonly DependencyProperty ContentProperty = DependencyProperty.RegisterAttached(
        "Content", typeof(Content), typeof(GetValueBasedOnContentSelectorBehavior), new PropertyMetadata(default(Content), OnContentChanged));

    private static void OnContentChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
        var textBox = dependencyObject as TextBox;
        if (textBox == null) return;

        var oldContent = dependencyPropertyChangedEventArgs.OldValue as Content;
        if (oldContent != null && Map.Remove(oldContent))
            oldContent.PropertyChanged -= ContentOnPropertyChanged;

        var newContent = dependencyPropertyChangedEventArgs.NewValue as Content;
        if (newContent != null)
        {
            newContent.PropertyChanged += ContentOnPropertyChanged;
            Map.Add(newContent, textBox);

            RedoBinding(textBox, newContent);
        }
    }

    private static void ContentOnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        var content = sender as Content;
        if (content == null) return;

        TextBox textBox;
        if (args.PropertyName == "Selector" && Map.TryGetValue(content, out textBox))
            RedoBinding(textBox, content);
    }

    private static void RedoBinding(TextBox textBox, Content content)
    {
        textBox.SetBinding(TextBox.TextProperty,
            new Binding { Source = content, Path = new PropertyPath("Value" + content.Selector) });
    }

    public static Content GetContent(TextBox txtBox)
    {
        return (Content)txtBox.GetValue(ContentProperty);
    }

    public static void SetContent(TextBox txtBox, Content value)
    {
        txtBox.SetValue(ContentProperty, value);
    }
}

how it's used:

<TextBox testWpf:GetValueBasedOnContentSelectorBehavior.Content="{Binding ContentInstance}"/>
<ComboBox SelectedItem="{Binding ContentInstance.Selector}" ItemsSource="{Binding AvailableValues}"/>

and some other added things:

public class Content : INotifyPropertyChanged
{
    private int _selector;

    public int Selector
    {
        get { return _selector; }
        set
        {
            _selector = value;
            OnPropertyChanged();
        }
    }

    public int Value1 { get; set; }
    public int Value2 { get; set; }
    public int Value3 { get; set; }
    public int Value4 { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ViewModel
{
    public Content ContentInstance { get; set; }
    public IEnumerable<int> AvailableValues { get { return Enumerable.Range(1, 4); } }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM