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?

4

5 に答える 5

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}" />
于 2010-02-11T17:14:41.130 に答える
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

于 2010-02-14T16:44:45.680 に答える
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;
    }
}
于 2011-08-23T08:58:09.617 に答える
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}" />
于 2020-12-22T12:43:11.190 に答える
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}" />
于 2021-02-13T09:16:13.210 に答える