MVVM 和 TextBox 的 SelectedText 属性

Posted

技术标签:

【中文标题】MVVM 和 TextBox 的 SelectedText 属性【英文标题】:MVVM and the TextBox's SelectedText property 【发布时间】:2011-01-15 19:10:41 【问题描述】:

我有一个带有 ContextMenu 的 TextBox。当用户在 TextBox 内右键单击并选择适当的 MenuItem 时,我想在我的视图模型中获取 SelectedText。我还没有找到一种“MVVM”方式的好方法。

到目前为止,我的应用程序使用 Josh Smith 的 MVVM 方式。我正在寻找转移到Cinch。不确定 Cinch 框架是否会处理此类问题。想法?

【问题讨论】:

【参考方案1】:

没有直接的方法将 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" />

【讨论】:

谢谢!!这成功了。如此明显,我错过了。再次感谢。 我正在尝试对 CaretIndex 属性做同样的事情,但它似乎不起作用。你能帮忙吗 @TheITGuy,不是没有看到您的代码...您可能应该创建一个新问题(您可以在此处发布链接,如果可以,我会回答) 请注意,这对我来说还不够(WPF 3.5 SP1)。为了触发 SelectedTextChanged 方法,我必须将 VM 中的 SelectedText 属性显式设置为不同于 null 的值。 @Andy,我想你误解了这段代码的作用。它不会更改选择文本的哪一部分,它会更改所选范围的内容。因此,如果选择为空,设置SelectedText 只是在插入符号位置插入一些文本;如果选择不为空,设置SelectedText 将替换选择的内容。这与“正常”TextBox.SelectedText 属性的行为相同。【参考方案2】:

WPF Application Framework (WAF) 中的示例应用程序选择了另一种方法来解决此问题。那里允许 ViewModel 通过接口 (IView) 访问 View,因此它可以请求当前的 SelectedText。

我认为不应该在所有场景中都使用绑定。有时在后面写几行代码比使用高级助手类要干净得多。但这只是我的看法:-)

jbe

【讨论】:

此解决方案的优点是能够使用公共设置器将所选文本的值推送到任何字符串属性上。可以说,灵活性超过了额外的代码行。此外,通过一些小的调整,该解决方案可用于绑定 SelectionStart 和 SelectionEnd 属性,从而允许视图模型轻松设置和接收文本选择。【参考方案3】:

我知道它已被回答并接受,但我想我会添加我的解决方案。我使用 Behavior 在视图模型和 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;
    

【讨论】:

【参考方案4】:

对于使用Stylet MVVM Framework 的任何人,都可以利用它通过“操作”将事件绑定到 ViewModel 方法的支持来实现这一点(尽管有些人可能认为这有点 hacky)。

您需要处理的 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 中,通过 Stylet Action 标记将事件挂钩到此方法:

xmlns:s="https://github.com/canton7/Stylet"
...
<TextBox SelectionChanged="s:Action OnTextSelectionChanged" />

【讨论】:

【参考方案5】:

正如 Timores 在对 Thomas Levesque 解决方案的评论中指出的那样,当视图模型中的属性未更改时,可能永远不会发生对 FrameworkPropertyMetadata 的 propertyChangedCallback 的初始调用的问题。 仅当 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" />

【讨论】:

以上是关于MVVM 和 TextBox 的 SelectedText 属性的主要内容,如果未能解决你的问题,请参考以下文章

如何连接 TextBox 的 TextChanged 事件和命令以便在 Silverlight 中使用 MVVM 模式

Mvvm KeyDown的实现以及TextBox绑定的属性不更新问题的解决

WPF MVVM:如何根据事件更新 UI 控制器

MVVM DEVDataColumn中的TextBox与ComboBox的并存

使用 MVVM 时,如何使 TextBox 成为“密码框”并显示星星?

WPF MVVM模式,有两个ListBox和一个TxtBox,选任一个ListBox的Item ,就显示在TextBox上。请帮帮高手。。