MVVM и свойство SelectedText TextBox

У меня есть TextBox с ContextMenu в нем. Когда пользователь щелкает правой кнопкой мыши внутри TextBox и выбирает соответствующий MenuItem, я хотел бы получить SelectedText в моей модели представления. Я не нашел хороший способ сделать это "MVVM".

До сих пор у меня есть заявка, использующая способ MVVM Джоша Смита. Я хочу перебраться в Cinch. Не уверен, что фреймворк Cinch справится с такими проблемами. Мысли?

5 ответов

Решение

Нет простого способа привязать SelectedText к источнику данных, потому что это не DependencyProperty... однако, довольно легко создать присоединенное свойство, которое вы могли бы связать вместо этого.

Вот базовая реализация:

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

}

Затем вы можете использовать это в XAML:

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

Примеры приложений в WPF Application Framework (WAF) выбрали другой способ решения этой проблемы. Там ViewModel разрешен доступ к View через интерфейс (IView), и поэтому он может запросить текущий SelectedText.

Я считаю, что связывание не должно использоваться в каждом сценарии. Иногда написание нескольких строк кода намного чище, чем использование высокоразвитых вспомогательных классов. Но это только мое мнение:-)

JBE

Я знаю, что на него ответили и приняли, но я думал, что добавлю свое решение. Я использую Поведение, чтобы соединить модель представления с TextBox. Поведение имеет свойство зависимостей (CaretPositionProperty), которое можно связать двумя способами с моделью представления. Внутренне поведение имеет дело с обновлениями в / из 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;
    }
}

Как указал Тиморес в комментарии к решению Томаса Левеска, существует проблема, заключающаяся в том, что первоначальный вызов propertyChangedCallback для FrameworkPropertyMetadata может никогда не произойти, если свойство в модели представления не изменено.
Проблема возникает только тогда, когда значение по умолчанию для FrameworkPropertyMetadata соответствует значению свойства в модели представления. Я решил это, используя случайное значение по умолчанию, которое вряд ли будет соответствовать значению в модели представления.

Код:

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

Любой, кто использует Stylet MVVM Framework, может добиться этого, воспользовавшись его поддержкой для привязки событий к методам ViewModel через «действие» (хотя некоторые могут счесть это немного хакерским).

Событие TextBox , которое вам нужно обработать,SelectionChanged. Создайте в ViewModel подходящий метод для обработки этого события:

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

Затем в XAML привяжите событие к этому методу через стилет. Actionразметка:

      xmlns:s="https://github.com/canton7/Stylet"
...
<TextBox SelectionChanged="{s:Action OnTextSelectionChanged}" />
Другие вопросы по тегам