MVVM and the TextBox's SelectedText property

2019-01-14 13:57发布

问题:

I have a TextBox with a ContextMenu in it. When the user right clicks inside the TextBox and chooses the appropriate MenuItem, I would like to grab the SelectedText in my viewmodel. I have not found a good way to do this the "MVVM" way.

So far I have my appliction utilizing Josh Smith's way of MVVM. I am looking to tranfer over to Cinch. Not sure if the Cinch framework will handle issues like this. Thoughts?

回答1:

There's no straightforward way to bind SelectedText to a data source, because it's not a DependencyProperty... however, it quite easy to create an attached property that you could bind instead.

Here's a basic implementation :

public static class TextBoxHelper
{

    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);
        }
    }

}

You can then use it like that in XAML :

<TextBox Text="{Binding Message}" u:TextBoxHelper.SelectedText="{Binding SelectedText}" />


回答2:

The sample applications in the WPF Application Framework (WAF) chose another way to solve this issue. There the ViewModel is allowed to access the View through an interface (IView) and so it can request the current SelectedText.

I believe Binding shouldn’t be used in every scenario. Sometimes writing a few lines in code behind is much cleaner than using highly advanced helper classes. But that’s just my opinion :-)

jbe



回答3:

I know it's been answered and accepted, but I thought I would add my solution. I use a Behavior to bridge between the view model and the TextBox. The behavior has a dependency property (CaretPositionProperty) which can be bound two way to the view model. Internally the behavior deals with the updates to/from the TextBox.

public class SetCaretIndexBehavior : Behavior<TextBox>
    {
        public static readonly DependencyProperty CaretPositionProperty;
        private bool _internalChange;

    static SetCaretIndexBehavior()
    {

    CaretPositionProperty = DependencyProperty.Register("CaretPosition", typeof(int), typeof(SetCaretIndexBehavior), new PropertyMetadata(0, OnCaretPositionChanged));
}

public int CaretPosition
{
    get { return Convert.ToInt32(GetValue(CaretPositionProperty)); }
    set { SetValue(CaretPositionProperty, value); }
}

protected override void OnAttached()
{
    base.OnAttached();
    AssociatedObject.KeyUp += OnKeyUp;
}

private static void OnCaretPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var behavior = (SetCaretIndexBehavior)d;
    if (!behavior._internalChange)
    {
        behavior.AssociatedObject.CaretIndex = Convert.ToInt32(e.NewValue);
    }
}

    private void OnKeyUp(object sender, KeyEventArgs e)
    {
        _internalChange = true;
        CaretPosition = AssociatedObject.CaretIndex;
        _internalChange = false;
    }
}