如何绑定到文本框的CaretIndex或光标位置

6

您好,我正在尝试绑定到TextBox.CaretIndex属性,但它不是一个DependencyProperty,所以我创建了一个Behavior,但它的效果不如预期。

期望(当获取焦点时)

  • 默认值为0
  • 如果我在视图中更改该值,则应更改我的视图模型中的值
  • 如果我在视图模型中更改该值,则应更改我的视图中的值

当前行为

  • 当窗口打开时,会调用视图模型值一次

代码后台

public class TextBoxBehavior : DependencyObject
{
    public static readonly DependencyProperty CursorPositionProperty =
        DependencyProperty.Register(
            "CursorPosition",
            typeof(int),
            typeof(TextBoxBehavior),
            new FrameworkPropertyMetadata(
                default(int),
                new PropertyChangedCallback(CursorPositionChanged)));

    public static void SetCursorPosition(DependencyObject dependencyObject, int i)
    {
        // breakpoint get never called
        dependencyObject.SetValue(CursorPositionProperty, i); 
    }

    public static int GetCursorPosition(DependencyObject dependencyObject)
    {
        // breakpoint get never called
        return (int)dependencyObject.GetValue(CursorPositionProperty);
    }

    private static void CursorPositionChanged(
        DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        // breakpoint get never called
        //var textBox = dependencyObject as TextBox;
        //if (textBox == null) return;
    }
}

XAML

<TextBox Text="{Binding TextTemplate,UpdateSourceTrigger=PropertyChanged}"
         local:TextBoxBehavior.CursorPosition="{Binding CursorPosition}"/>

更多信息

我认为这里有些问题,因为我需要从DependencyObject中派生它,而以前从未需要过,因为CursorPositionProperty已经是一个DependencyProperty,所以这应该足够了。我还认为我需要在我的Behavior中使用一些事件来正确设置我的CursorPositionProperty,但我不知道要用哪些。

4个回答

7

在与我的行为斗争后,我可以向您呈现一个99%的工作解决方案。

行为

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfMVVMTextBoxCursorPosition
{
    public class TextBoxCursorPositionBehavior : DependencyObject
    {
        public static void SetCursorPosition(DependencyObject dependencyObject, int i)
        {
            dependencyObject.SetValue(CursorPositionProperty, i);
        }

        public static int GetCursorPosition(DependencyObject dependencyObject)
        {
            return (int)dependencyObject.GetValue(CursorPositionProperty);
        }

        public static readonly DependencyProperty CursorPositionProperty =
                                           DependencyProperty.Register("CursorPosition"
                                                                       , typeof(int)
                                                                       , typeof(TextBoxCursorPositionBehavior)
                                                                       , new FrameworkPropertyMetadata(default(int))
                                                                       {
                                                                           BindsTwoWayByDefault = true
                                                                           ,DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
                                                                       }
                                                                       );

        public static readonly DependencyProperty TrackCaretIndexProperty =
                                                    DependencyProperty.RegisterAttached(
                                                        "TrackCaretIndex",
                                                        typeof(bool),
                                                        typeof(TextBoxCursorPositionBehavior),
                                                        new UIPropertyMetadata(false
                                                                                , OnTrackCaretIndex));

        public static void SetTrackCaretIndex(DependencyObject dependencyObject, bool i)
        {
            dependencyObject.SetValue(TrackCaretIndexProperty, i);
        }

        public static bool GetTrackCaretIndex(DependencyObject dependencyObject)
        {
            return (bool)dependencyObject.GetValue(TrackCaretIndexProperty);
        }

        private static void OnTrackCaretIndex(DependencyObject dependency, DependencyPropertyChangedEventArgs e)
        {
            var textbox = dependency as TextBox;

            if (textbox == null)
                return;
            bool oldValue = (bool)e.OldValue;
            bool newValue = (bool)e.NewValue;

            if (!oldValue && newValue) // If changed from false to true
            {
                textbox.SelectionChanged += OnSelectionChanged;
            }
            else if (oldValue && !newValue) // If changed from true to false
            {
                textbox.SelectionChanged -= OnSelectionChanged;
            }
        }

        private static void OnSelectionChanged(object sender, RoutedEventArgs e)
        {
            var textbox = sender as TextBox;

            if (textbox != null)
                SetCursorPosition(textbox, textbox.CaretIndex); // dies line does nothing
        }
    }
}

XAML

    <TextBox Height="50" VerticalAlignment="Top"
             Name="TestTextBox"
             Text="{Binding MyText}"
             vm:TextBoxCursorPositionBehavior.TrackCaretIndex="True"
             vm:TextBoxCursorPositionBehavior.CursorPosition="{Binding CursorPosition,Mode=TwoWay}"/>

    <TextBlock Height="50" Text="{Binding CursorPosition}"/>

有一件事我不明白,为什么BindsTwoWayByDefault = true没有起作用。据我观察,它对绑定没有任何影响,因此我需要在XAML中显式设置绑定模式。


1
public static int GetTrackCaretIndex(DependencyObject dependencyObject) 应该返回bool,因为它是一个布尔属性。+ 否则会出现 "输入字符串不符合正确格式" 的 XAML 错误。return (bool)dependencyObject.GetValue(TrackCaretIndexProperty); - Jedzia
CursorPosition 也需要使用 RegisterAttached。 - user3391859
2
这段代码在选择更改时完美地设置了videmodel属性,但是当我从代码中更改viewmodel属性时,它不会更改UI上的caretindex。有什么遗漏吗? - Tejas Vaishnav

5

我遇到了类似的问题,对我来说最简单的解决方案是继承自TextBox并添加一个DependencyProperty。所以它看起来像这样:

namespace UI.Controls
{
    public class MyTextBox : TextBox
    {
        public static readonly DependencyProperty CaretPositionProperty =
            DependencyProperty.Register("CaretPosition", typeof(int), typeof(MyTextBox),
                new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnCaretPositionChanged));

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

        public MyTextBox()
        {
            SelectionChanged += (s, e) => CaretPosition = CaretIndex;
        }

        private static void OnCaretPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as MyTextBox).CaretIndex = (int)e.NewValue;
        }
    }
}

在我的XAML中:

xmlns:controls="clr-namespace:IU.Controls"
...
<controls:MyTextBox CaretPosition="{Binding CaretPosition}"/>

当然,如果您不打算将视图模型绑定到其他文本编辑控件,则只使用View Model中的Text和CaretPosition属性可能就足够了。如果需要绑定到其他文本编辑控件,则可能需要另一种解决方案。


2
WiiMaxx的解决方案对我来说存在以下问题:
  1. 当视图模型属性从代码中更改时,文本框中的插入符索引未更改。这也被Tejas Vaishnav在他对该解决方案的评论中提到。
  2. BindsTwoWayByDefault = true无法工作。
  3. 他表示需要继承DependencyObject很奇怪。
  4. TrackCaretIndex属性仅用于初始化,并且感觉有点不必要。
以下是我的解决方案,可以解决这些问题: 行为
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 int CaretIndexPropertyDefault = -485609317;

    public static void SetCaretIndex(DependencyObject dependencyObject, int i)
    {
        dependencyObject.SetValue(CaretIndexProperty, i);
    }

    public static int GetCaretIndex(DependencyObject dependencyObject)
    {
        return (int)dependencyObject.GetValue(CaretIndexProperty);
    }

    public static readonly DependencyProperty CaretIndexProperty =
        DependencyProperty.RegisterAttached(
            "CaretIndex",
            typeof(int),
            typeof(TextBoxAssist),
            new FrameworkPropertyMetadata(
                CaretIndexPropertyDefault,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                CaretIndexChanged));

    private static void CaretIndexChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
    {
        if (dependencyObject is not TextBox textBox || eventArgs.OldValue is not int oldValue || eventArgs.NewValue is not int newValue)
        {
            return;
        }

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

        if (newValue != textBox.CaretIndex)
        {
            textBox.CaretIndex = newValue;
        }
    }

    private static void SelectionChangedForCaretIndex(object sender, RoutedEventArgs eventArgs)
    {
        if (sender is TextBox textBox)
        {
            SetCaretIndex(textBox, textBox.CaretIndex);
        }
    }

}

XAML

    <TextBox Height="50" VerticalAlignment="Top"
             Name="TestTextBox"
             Text="{Binding MyText}"
             viewModels:TextBoxAssist.CaretIndex="{Binding CaretIndex}"/>

一些区别的澄清:
  • 视图模型属性更改现在可以工作,因为TextBox上的插入符索引在CaretIndexChanged结束时设置。
  • 通过使用相应的FrameworkPropertyMetadata构造函数参数来修复了BindsTwoWayByDefault
  • DependencyObject继承仅是必要的,因为使用了DependencyProperty.Register而不是DependencyProperty.RegisterAttached
  • 没有TrackCaretIndex属性,我遇到了一个问题,即FrameworkPropertyMetadatapropertyChangedCallback从未被调用以正确初始化事物。该问题只出现在FrameworkPropertyMetadata的默认值与视图模型属性的值在开始时完全匹配且视图模型属性未更改时。这就是为什么我使用这个随机默认值。

0

正如您所说, TextBox.CaretIndex 属性不是DependencyProperty,因此您无法进行数据绑定。即使使用自己的DependencyProperty,它也不起作用……您希望在TextBox.CaretIndex属性更改时如何通知?


我认为我可以使用一些事件来改变值。 - WiiMaxx
当然,您可以更改该值,但是当它自己更改时,您将不会收到通知。但是,您不需要一个“DependencyProperty”仅仅为了更改该值。 - Sheridan
好的,我明白了。那么你会如何尝试获取 TextBox.CaretIndex - WiiMaxx
@WiiMaxx,你可以使用行为(behavior)来存储TextBox实例、记忆当前位置、启动计时器并轮询更改(我不确定如何将更改传递给视图模型和返回)。同时订阅Unloaded事件以停止计时器。 - Sinatr
@Sheridan,如果您能看一下我的答案并告诉我BindsTwoWayByDefault = true的问题所在,那就太好了。提前感谢您。 - WiiMaxx
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接