2

In trying to solve an issue Im having in another project - Ive created the following example to replicate the issue.

The idea is that when the user enters new values, via the slider or textbox, those values should then be "ConvertedBack" via the convertor, and the source updated. I don't seem to be seeing this though, I believe due to the fact that InternalRep's properties are being written to, but not informing the bindexpression for the InternalRepProperty.

What is the best way to go about solving this problem?

One method I tried was to handle the sliders ValueChanged event, but this caused the convertor to ... ConvertBack then Convert then ConvertBack then Convert, not sure why.

When the user changes a value, I need the convertor to only ConvertBack to update the source, and nothing else, .. is this possible?

TextSplitter XAML

<ContentControl x:Class="WpfApplication23.TextSplitter"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:WpfApplication23"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <UniformGrid Columns="3" Rows="2">
        <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.First, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />

        <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.Second, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />

        <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.Third, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />

        <Slider  Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.First, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" />

        <Slider  Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.Second, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" />

        <Slider  Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:TextSplitter}}, 
                 Path=InternalRep.Third, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Maximum="255" ValueChanged="OnSliderChnaged" />
    </UniformGrid>

</ContentControl>

TextSplitter C#

public class InternalRep
    {
        public int First { get; set; }
        public int Second { get; set; }
        public int Third { get; set; }
    };

    public class LettersToInternalRepMultiConvertor : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType,
               object parameter, System.Globalization.CultureInfo culture)
        {
            InternalRep ir = new InternalRep()
            {
                First = (int)(char)values[0],
                Second = (int)(char)values[1],
                Third = (int)(char)values[2],
            };

            return ir;
        }

        public object[] ConvertBack(object value, Type[] targetTypes,
               object parameter, System.Globalization.CultureInfo culture)
        {
            InternalRep ir = (InternalRep)value;
            if (ir != null)
            {
                return new object[] 
                { 
                    (char)ir.First, 
                    (char)ir.Second, 
                    (char)ir.Third 
                };
            }
            else
            {
                throw new Exception();
            }
        }
    }

    public partial class TextSplitter : ContentControl
    {
        public static readonly DependencyProperty FirstProperty = DependencyProperty.Register(
        "First", typeof(char), typeof(TextSplitter));

        public static readonly DependencyProperty SecondProperty = DependencyProperty.Register(
        "Second", typeof(char), typeof(TextSplitter));

        public static readonly DependencyProperty ThirdProperty = DependencyProperty.Register(
        "Third", typeof(char), typeof(TextSplitter));

        public static readonly DependencyProperty InternalRepProperty = DependencyProperty.Register(
        "InternalRep", typeof(InternalRep), typeof(TextSplitter));

        BindingExpressionBase beb = null;

        public TextSplitter()
        {
            InitializeComponent();

            MultiBinding mb = new MultiBinding();
            mb.Mode = BindingMode.TwoWay;
            mb.Bindings.Add(SetBinding("First"));
            mb.Bindings.Add(SetBinding("Second"));
            mb.Bindings.Add(SetBinding("Third"));
            mb.Converter = new LettersToInternalRepMultiConvertor();

            beb = SetBinding(InternalRepProperty, mb);
        }

        Binding SetBinding(string _property)
        {
            Binding b = new Binding(_property);
            b.Mode = BindingMode.TwoWay;
            b.Source = this;
            return b;
        }

        public char First
        {
            get { return (char)GetValue(FirstProperty); }
            set { SetValue(FirstProperty, value); }
        }

        public char Second
        {
            get { return (char)GetValue(SecondProperty); }
            set { SetValue(SecondProperty, value); }
        }

        public char Third
        {
            get { return (char)GetValue(ThirdProperty); }
            set { SetValue(ThirdProperty, value); }
        }
    }

MainWindow XAML

<Window x:Class="WpfApplication23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication23"
        Title="MainWindow" Height="640" Width="800" WindowStartupLocation="CenterScreen">
    <StackPanel>
        <local:TextSplitter First="{Binding A, Mode=TwoWay}"
                            Second="{Binding B, Mode=TwoWay}"
                            Third="{Binding C, Mode=TwoWay}"/>
    </StackPanel>
</Window>

MainWindow Code

namespace WpfApplication23
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        char m_a = 'a';
        public char A
        {
            get { return m_a; }
            set { m_a = value; }
        }

        char m_B = 'b';
        public char B
        {
            get { return m_B; }
            set{ m_B = value; }
        }

        char m_c = 'c';
        public char C
        {
            get { return m_c; }
            set { m_c = value; }
        }

    }
}
wforl
  • 843
  • 10
  • 22
  • What does your `OnSliderChnaged` method do? Because you have `TwoWay` bindings, `ConvertBack` will already be called once, so calling it a 2nd time in your `ValueChanged` event will make it run twice. Also, I'm still trying to figure out your `MultiBinding` in the code behind, but it's possible that is making it run twice too. – Rachel Nov 27 '12 at 15:43
  • The OnSliderChnaged method isnt implemented, it was just left over from some experimentation, the whole application is as I posted above. The problem is that convertback is never being called when a user edits the sliders/textboxes, which is what I'm trying to solve/remedy – wforl Nov 27 '12 at 21:44
  • 1
    I'm starting to think there is an architectural problem. What exactly are you trying to achieve by getting the setters to be called? Why would all 3 setters be called when one Slider bound to one variable changes? Are you making calculations? – Louis Kottmann Nov 27 '12 at 22:11
  • The example code I posted is there to demonstrate the problem Im having with multi-bindings. All three setters would be called in the convertback because thats a how a multi-converter works is it not? – wforl Nov 27 '12 at 22:55

1 Answers1

0

You need to implement INotifyPropertyChanged in your ViewModel.
Here, the ViewModel is the Window , so you have to do:

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        this.DataContext = this;
        InitializeComponent();
    }

    char m_a = 'a';
    public char A
    {
        get { return m_a; }
        set
        {
            if (value != m_a)
            {
                m_c = value;
                RaisePropertyChanged("A");
            }
        }
    }

    char m_B = 'b';
    public char B
    {
        get { return m_B; }
        set
        {
            if (value != m_B)
            {
                m_c = value;
                RaisePropertyChanged("B");
            }
        }
    }

    char m_c = 'c';
    public char C
    {
        get { return m_c; }
        set
        {
            if (value != m_c)
            {
                m_c = value;
                RaisePropertyChanged("C");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void RaisePropertyChanged(string _Prop)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(_Prop));
        }
    }

    DelegateCommand _RecomputeCommand;
    public DelegateCommand RecomputeCommand
    {
        get { return _RecomputeCommand ?? (_RecomputeCommand = new DelegateCommand(Recompute)); }
    }

    public void Recompute()
    {
        //Do something with A, B and C.
    }
}

EDIT: you should simply bind the 3 sliders to A, B, C (you'll need the above implementation) and then act upon a RecomputeCommand like so:

<StackPanel>
    <Slider Value="{Binding A}" Maximum="255">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ValueChanged">
                <i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Slider>
    <Slider Value="{Binding B}" Maximum="255">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ValueChanged">
                <i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Slider>
    <Slider Value="{Binding C}" Maximum="255">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="ValueChanged">
                <i:InvokeCommandAction Command="{Binding RecomputeCommand}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </Slider>
</StackPanel>

Of course this can be in a ContentControl as needed.

Louis Kottmann
  • 16,268
  • 4
  • 64
  • 88
  • I dont see how that helps me, the problem is that the values arent being written back to the source in the first place. The conversion works the one way, by converting source to target, .. but when a user chnages the target, the source isnt being updated – wforl Nov 27 '12 at 13:11
  • 1
    @wforl How does anything get notified if you don't raise PropertyChanged? Whenever you write `Binding`, the Path must point to a PropertyChanged-ready property otherwise the value is retrieved only at initialization. – Louis Kottmann Nov 27 '12 at 13:24
  • @ baboon. The problem isn't with the value being retrieved, .. I actually want that to only be read at initialisation. The problem is that TextSplitterControl isnt writing back to MainWindow.A | MainWindow.B | MainWindow.C – wforl Nov 27 '12 at 13:29
  • @wforl did you try my solution? – Louis Kottmann Nov 27 '12 at 15:26
  • I did try it, and it didnt solve my problem. Maybe I'm not being clear about what Im after. I want the properties A,B,C in mainwindow to have their setters called when the user is interacting with the Sliders/Textboxes. If the setters arent being called in the first place, how would your solution chnage anything? The ConvertBack function isnt being called when the values in textboxes/sliders are chnaging – wforl Nov 27 '12 at 18:09
  • @wforl ok before i try to recreate this myself, what is the purpose of the `IMultiValueConverter`? you're not doing anything with the `beb` variable nor with the result of `Convert`/`ConvertBack`. – Louis Kottmann Nov 27 '12 at 18:53
  • as I explained in the first post, this example was just to replicate an issue I was having, hence the reason for me not doing anything with the result. I simply want the the setters of A,B,C in mainwindow to be called when a user edits the sliders/textboxes. Im setting breakpoints for them and they dont get called. The beb variable was just left over from some experimentation, I should have removed that, sorry. – wforl Nov 27 '12 at 21:36
  • You've completely bypassed the TextSplitter class. The textsplitter class is there to represent the setup I have in a more complicated example that also uses Multi-Bindings, simplified to demonstrate the problem. I dont want to set A,B,C directly with the sliders, they should be updated through the binding in the textsplitter class. – wforl Nov 27 '12 at 22:52
  • @wforl I just showed an example, you can replicate all that with the `TextSplitter` class easily but it's left as an exercise. My core point is to use the interactivity library. – Louis Kottmann Nov 28 '12 at 09:34