Search code examples
wpfdata-bindingmvvmattached-properties

MVVM- How can I bind to a property, which is not a DependancyProperty?


I have found this question MVVM and the TextBox's SelectedText property. However, I am having trouble getting the solution given to work. This is my non-working code, in which I am trying to display the first textbox's selected text in the second textbox.

View:

SelectedText and Text are just string properties from my ViewModel.

<TextBox Text="{Binding Path=Text, UpdateSourceTrigger=PropertyChanged}"  Height="155" HorizontalAlignment="Left" Margin="68,31,0,0" Name="textBox1" VerticalAlignment="Top" Width="264" AcceptsReturn="True" AcceptsTab="True" local:TextBoxHelper.SelectedText="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" />
        <TextBox Text="{Binding SelectedText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" Height="154" HorizontalAlignment="Left" Margin="82,287,0,0" Name="textBox2" VerticalAlignment="Top" Width="239" />

TextBoxHelper

 public static class TextBoxHelper
{
    #region "Selected Text"
    public static string GetSelectedText(DependencyObject obj)
    {
        return (string)obj.GetValue(SelectedTextProperty);
    }

    public static void SetSelectedText(DependencyObject obj, string value)
    {
        obj.SetValue(SelectedTextProperty, value);
    }

    // Using a DependencyProperty as the backing store for SelectedText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedTextProperty =
        DependencyProperty.RegisterAttached(
            "SelectedText",
            typeof(string),
            typeof(TextBoxHelper),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedTextChanged));

    private static void SelectedTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        TextBox tb = obj as TextBox;
        if (tb != null)
        {
            if (e.OldValue == null && e.NewValue != null)
            {
                tb.SelectionChanged += tb_SelectionChanged;
            }
            else if (e.OldValue != null && e.NewValue == null)
            {
                tb.SelectionChanged -= tb_SelectionChanged;
            }

            string newValue = e.NewValue as string;

            if (newValue != null && newValue != tb.SelectedText)
            {
                tb.SelectedText = newValue as string;
            }
        }
    }

    static void tb_SelectionChanged(object sender, RoutedEventArgs e)
    {
        TextBox tb = sender as TextBox;
        if (tb != null)
        {
            SetSelectedText(tb, tb.SelectedText);
        }
    }
    #endregion

}

What am I doing wrong?


Solution

  • The reason this is not working is that the property change callback isn't being raised (as the bound value from your VM is the same as the default value specified in the metadata for the property). More fundamentally though, your behavior will detach when the selected text is set to null. In cases like this, I tend to have another attached property that is simply used to enable the monitoring of the selected text, and then the SelectedText property can be bound. So, something like so:

    #region IsSelectionMonitored
    public static readonly DependencyProperty IsSelectionMonitoredProperty = DependencyProperty.RegisterAttached(
        "IsSelectionMonitored",
        typeof(bool),
        typeof(PinnedInstrumentsViewModel),
        new FrameworkPropertyMetadata(OnIsSelectionMonitoredChanged));
    
    [AttachedPropertyBrowsableForType(typeof(TextBox))]
    public static bool GetIsSelectionMonitored(TextBox d)
    {
        return (bool)d.GetValue(IsSelectionMonitoredProperty);
    }
    
    public static void SetIsSelectionMonitored(TextBox d, bool value)
    {
        d.SetValue(IsSelectionMonitoredProperty, value);
    }
    
    private static void OnIsSelectionMonitoredChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        TextBox tb = obj as TextBox;
        if (tb != null)
        {
            if ((bool)e.NewValue)
            {
                tb.SelectionChanged += tb_SelectionChanged;
            }
            else
            {
                tb.SelectionChanged -= tb_SelectionChanged;
            }
    
            SetSelectedText(tb, tb.SelectedText);
        }
    }
    #endregion
    
    #region "Selected Text"
    public static string GetSelectedText(DependencyObject obj)
    {
        return (string)obj.GetValue(SelectedTextProperty);
    }
    
    public static void SetSelectedText(DependencyObject obj, string value)
    {
        obj.SetValue(SelectedTextProperty, value);
    }
    
    // Using a DependencyProperty as the backing store for SelectedText.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedTextProperty =
        DependencyProperty.RegisterAttached(
            "SelectedText",
            typeof(string),
            typeof(TextBoxHelper),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedTextChanged));
    
    private static void SelectedTextChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        TextBox tb = obj as TextBox;
        if (tb != null)
        {
            tb.SelectedText = e.NewValue as string;            
        }
    }
    
    static void tb_SelectionChanged(object sender, RoutedEventArgs e)
    {
        TextBox tb = sender as TextBox;
        if (tb != null)
        {
            SetSelectedText(tb, tb.SelectedText);
        }
    }
    #endregion
    

    And then in your XAML, you'd have to add that property to your first TextBox:

    <TextBox ... local:TextBoxHelper.IsSelectionMonitored="True" local:TextBoxHelper.SelectedText="{Binding SelectedText, Mode=OneWayToSource}" />