Justin
Justin

Reputation: 2479

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?

Upvotes: 1

Views: 3515

Answers (5)

Kent Boogaart
Kent Boogaart

Reputation: 178820

Your binding attempts to bind the Text property of your TextBox to a SelectedText property of the TextBox's current data context. Since you're working with an attached property, not with a property hanging off of your data context, you will need to give more information in your binding:

<TextBox Text="{Binding local:TextBoxHelper.SelectedText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" ... />

Where local has been associated with the CLR namespace containing the TextBoxHelper class.

Upvotes: 0

Wallstreet Programmer
Wallstreet Programmer

Reputation: 9687

This works for me using the class TextBoxHelper. As other mentioned, you need to initialize the SelectedText property of TextBoxHelper with a non null value. Instead of data binding to a string property (SelText) on the view you should bind to a string property of your VM which should implement INotifyPropertyChanged.

XAML:

<Window x:Class="TextSelectDemo.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TextSelectDemo"
    Height="300" Width="300">
    <StackPanel>
        <TextBox local:TextBoxHelper.SelectedText="{Binding Path=SelText, Mode=TwoWay}" />
        <TextBox Text="{Binding Path=SelText}" />
    </StackPanel>
</Window>

Code behind:

using System.ComponentModel;
using System.Windows;

namespace TextSelectDemo
{
    public partial class Window1 : Window, INotifyPropertyChanged
    {
        public Window1()
        {
            InitializeComponent();

            SelText = string.Empty;

            DataContext = this;
        }

        private string _selText;
        public string SelText
        {
            get { return _selText; }
            set
            {
                _selText = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelText"));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Upvotes: 1

Abe Heidebrecht
Abe Heidebrecht

Reputation: 30498

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}" />

Upvotes: 1

Richard McGuire
Richard McGuire

Reputation: 11100

In order for the SelectedTextChanged handler to fire the SelectedText property must have an initial value. If you don't initialize this to some value (string.Empty as a bare minimum) then this handler will never fire and in turn you'll never register the tb_SelectionChanged handler.

Upvotes: 1

Codism
Codism

Reputation: 6236

You need a normal .net property wrapper for the dependencyproperty, some like:

public string SelectedText
{
   set {SetSelectedText(this, value);}
...

It is not required by runtime (runtime use set/get) but it is required by designer and compiler.

Upvotes: -1

Related Questions