如何绑定到文本框的 CaretIndex aka 光标位置
How to Bind to CaretIndex aka curser position of an Textbox
您好,我正在尝试绑定到 TextBox.CaretIndex
属性,它不是 DependencyProperty
,所以我创建了 Behavior
,但它不是按预期工作。
期望(专注时)
- 默认值 = 0
- 如果我更改 view 中的值,它应该更改 viewmodel
中的值
- 如果我更改 viewmodel 中的值,它应该更改 view
中的值
当前行为
- viewmodel 值在 window 打开时被调用
代码隐藏
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
,但我不知道是哪个。
正如您所说,TextBox.CaretIndex
Property 是 而不是 和 DependencyProperty
,因此您无法对其进行数据绑定。即使使用您自己的 DependencyProperty
,它也不起作用...当 TextBox.CaretIndex
属性 发生变化时,您希望如何收到通知?
在与我的行为作斗争之后,我可以向您展示一个 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
中显式设置绑定模式
我遇到了类似的问题,对我来说最简单的解决方案是继承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}"/>
... 和 CaretPosition 属性 当然在视图模型中。如果您不打算将您的视图模型绑定到其他文本编辑控件,这可能就足够了,如果是 - 您可能需要另一个解决方案。
WiiMaxx 的解决方案对我来说有以下问题:
- 当从代码更改视图模型 属性 时,文本框中的插入符号索引不会更改。
Tejas Vaishnav 在对解决方案的评论中也提到了这一点。
BindsTwoWayByDefault = true
无效。
- 他说很奇怪他需要继承
DependencyObject
。
- 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
的末尾。
BindsTwoWayByDefault
已通过使用相应的 FrameworkPropertyMetadata
构造函数参数修复。
- 仅需要从
DependencyObject
继承,因为使用 DependencyProperty.Register
而不是 DependencyProperty.RegisterAttached
。
- 没有 TrackCaretIndex 属性 我遇到的问题是
FrameworkPropertyMetadata
的 propertyChangedCallback
从未被调用来正确初始化。仅当 FrameworkPropertyMetadata
的默认值从一开始就与视图模型 属性 的值匹配并且视图模型 属性 未更改时才会出现此问题。这就是我使用这个随机默认值的原因。
您好,我正在尝试绑定到 TextBox.CaretIndex
属性,它不是 DependencyProperty
,所以我创建了 Behavior
,但它不是按预期工作。
期望(专注时)
- 默认值 = 0
- 如果我更改 view 中的值,它应该更改 viewmodel 中的值
- 如果我更改 viewmodel 中的值,它应该更改 view 中的值
当前行为
- viewmodel 值在 window 打开时被调用
代码隐藏
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
,但我不知道是哪个。
正如您所说,TextBox.CaretIndex
Property 是 而不是 和 DependencyProperty
,因此您无法对其进行数据绑定。即使使用您自己的 DependencyProperty
,它也不起作用...当 TextBox.CaretIndex
属性 发生变化时,您希望如何收到通知?
在与我的行为作斗争之后,我可以向您展示一个 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
我遇到了类似的问题,对我来说最简单的解决方案是继承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}"/>
... 和 CaretPosition 属性 当然在视图模型中。如果您不打算将您的视图模型绑定到其他文本编辑控件,这可能就足够了,如果是 - 您可能需要另一个解决方案。
WiiMaxx 的解决方案对我来说有以下问题:
- 当从代码更改视图模型 属性 时,文本框中的插入符号索引不会更改。 Tejas Vaishnav 在对解决方案的评论中也提到了这一点。
BindsTwoWayByDefault = true
无效。- 他说很奇怪他需要继承
DependencyObject
。 - 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
的末尾。 BindsTwoWayByDefault
已通过使用相应的FrameworkPropertyMetadata
构造函数参数修复。- 仅需要从
DependencyObject
继承,因为使用DependencyProperty.Register
而不是DependencyProperty.RegisterAttached
。 - 没有 TrackCaretIndex 属性 我遇到的问题是
FrameworkPropertyMetadata
的propertyChangedCallback
从未被调用来正确初始化。仅当FrameworkPropertyMetadata
的默认值从一开始就与视图模型 属性 的值匹配并且视图模型 属性 未更改时才会出现此问题。这就是我使用这个随机默认值的原因。