MVVM and the TextBox's SelectedText property
Asked Answered
H

5

16

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?

Hachmin answered 11/2, 2010 at 16:33 Comment(0)
N
23

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}" />
Nursery answered 11/2, 2010 at 17:14 Comment(6)
Thanks you!! This did the trick. So obvious and I missed it. Thanks again.Hachmin
I am trying to do the same for CaretIndex property but it seems that its not working. Can you helpArria
@TheITGuy, not without seeing your code... You should probably create a new question (you can post the link here, I'll answer if I can)Nursery
Note that this was not enough in my case (WPF 3.5 SP1). I had to explicitly set the SelectedText property in the VM to something different than null in order to trigger the SelectedTextChanged method.Penile
Hoping someone can help :) I'm trying to use this solution in .NET 4 but every time I try to set the SelectedText property, the text is added to the textbox and nothing is selected. I am doing SelectedText = Message (sticking to your example)Eldredge
@Andy, I think you misunderstand what this code does. It doesn't change which part of the text is selected, it changes the content of the selected range. So if the selection is empty, setting SelectedText just inserts some text at the caret position; if the selection is not empty, setting SelectedText replaces the content of the selection. This is the same behavior as the "normal" TextBox.SelectedText property.Nursery
A
1

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

Aw answered 14/2, 2010 at 16:44 Comment(1)
This solution has the advantage of being able to push the value of the selected text on any string property with a public setter. The flexibility arguably outweighs the extra lines of code. In addition, with a few small tweaks, the solution could be used to bind SelectionStart and SelectionEnd properties, allowing view models to easily set, and receive the text selection.Expiate
C
1

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;
    }
}
Cicelycicenia answered 23/8, 2011 at 8:58 Comment(0)
H
0

For anyone using the Stylet MVVM Framework, it is possible to accomplish this by taking advantage of its support for binding events to ViewModel methods via an "action" (although some might consider it a little hacky).

The TextBox event that you need to handle is SelectionChanged. Create a suitable method in the ViewModel to handle this event:

public void OnTextSelectionChanged(object sender, RoutedEventArgs e)
{
    if (e.OriginalSource is TextBox textBox)
    {
        // Do something with textBox.SelectedText
        // Note: its value will be "" if no text is selected, not null
    }
}

Then, in the XAML, hook the event to this method via a Stylet Action markup:

xmlns:s="https://github.com/canton7/Stylet"
...
<TextBox SelectionChanged="{s:Action OnTextSelectionChanged}" />
Hizar answered 22/12, 2020 at 12:43 Comment(0)
P
0

As Timores pointed out in a comment on the solution from Thomas Levesque, there is a problem that the initial call to the propertyChangedCallback for the FrameworkPropertyMetadata might never happen when the property in the view model is not changed.
The problem occurs only when the default value for the FrameworkPropertyMetadata matches the property value in the view model. I solved that by using a random default value which should be very unlikely to match the value in the view model.

Code:

public static class TextBoxAssist
{

    // This strange default value is on purpose it makes the initialization problem very unlikely.
    // If the default value matches the default value of the property in the ViewModel,
    // the propertyChangedCallback of the FrameworkPropertyMetadata is initially not called
    // and if the property in the ViewModel is not changed it will never be called.
    private const string SelectedTextPropertyDefault = "pxh3949%lm/";

    public static string GetSelectedText(DependencyObject obj)
    {
        return (string)obj.GetValue(SelectedTextProperty);
    }

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

    public static readonly DependencyProperty SelectedTextProperty =
        DependencyProperty.RegisterAttached(
            "SelectedText",
            typeof(string),
            typeof(TextBoxAssist),
            new FrameworkPropertyMetadata(
                SelectedTextPropertyDefault,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                SelectedTextChanged));

    private static void SelectedTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
    {
        if (dependencyObject is not TextBox textBox)
        {
            return;
        }

        var oldValue = eventArgs.OldValue as string;
        var newValue = eventArgs.NewValue as string;

        if (oldValue == SelectedTextPropertyDefault && newValue != SelectedTextPropertyDefault)
        {
            textBox.SelectionChanged += SelectionChangedForSelectedText;
        }
        else if (oldValue != SelectedTextPropertyDefault && newValue == SelectedTextPropertyDefault)
        {
            textBox.SelectionChanged -= SelectionChangedForSelectedText;
        }

        if (newValue is not null && newValue != textBox.SelectedText)
        {
            textBox.SelectedText = newValue;
        }
    }

    private static void SelectionChangedForSelectedText(object sender, RoutedEventArgs eventArgs)
    {
        if (sender is TextBox textBox)
        {
            SetSelectedText(textBox, textBox.SelectedText);
        }
    }

}

XAML:

<TextBox Text="{Binding Message}" u:TextBoxAssist.SelectedText="{Binding SelectedText}" />
Petrozavodsk answered 13/2, 2021 at 9:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.