C#/VB.Net 中可高亮显示的 WPF/MVVM TextBlock





5.00/5 (12投票s)
在 WPF TextBlock 控件中实现具有 MVVM 的文本高亮
引言
我们回顾了一个 WPF 行为 [3],它可以高亮显示 WPF TextBlock
控件中的文本。这个行为几乎可以在任何使用 TextBlock
控件的地方使用。因此,虽然本文中我们将其用在 ListBox
中,但该行为也可以用于
- 不带集合控件或者
- 在任何表示集合的控件内(例如:
TreeView
- 请参阅 [1] 中的奖励内容)
我希望支持的其他要求是
- MVVM 架构模式
- 对黑色和浅色皮肤的默认支持
- 自定义高亮颜色配置
下面的截图展示了根据 [2] 中的想法开发的示例应用程序。你应该能够选择左侧列表中的一个 string
,然后右侧列表应该高亮显示相应的项目。
背景
我们开发了这个 WPF 树视图 [1] filter 应用程序,它可以在大约 100,000 个节点树上快速运行,我一直在想如何高亮显示一个 TextBlock
项目,因为有时很难真正看到匹配的文本。所以,我进行了一些搜索,找到了一些有前景的示例,没有一个支持 MVVM 或暗/浅色皮肤,但是 [2] 中的示例对我来说最有前景,所以我试图将这个解决方案开发成一个更健壮、更符合 MVVM 的解决方案。
Using the Code
本文附带的示例代码包含 2 个项目
WpfApplication4
HighlightableTextBlock
其中第一个 WpfApplication4
项目基本上是 [2] 中的原始代码,而 HighlightableTextBlock
项目包含了我想在本文中开发的这个行为。你应该能够将每个项目设置为 Visual Studio 中的启动项目,启动项目并点击列表框来感受这些示例的工作方式。
关注点
我附上了两个版本,希望这能帮助其他人感受到将标准(Winforms 兼容)解决方案转换为 MVVM 兼容解决方案所需的转换过程。
那么,让我们检查 HighlightableTextBlock
项目中的代码以了解它是如何工作的。MainWindow.xaml 文件中左侧 ListBox
的定义是一个很好的起点。
<ListBox Grid.Column="0" Grid.Row="1"
ItemsSource="{Binding List1}"
SelectionMode="Single"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="3"
behav:SelectedItemsBahavior.ChangedCommand="{Binding ListSelectionChangedCommand}"
/>
上面的 XAML 指定 ListBox
应从 AppViewModel
对象中的 List1
属性获取其项目。当左侧 ListBox
中的选择发生更改时,就会调用 SelectedItemsBehavior
。当用户点击 MainWindow
左侧列表中的项目时,该行为会调用 AppViewModel
对象中的 ListSelectionChangedCommand
(将选定的项目作为命令参数发送)。ListSelectionChangedCommand
中的代码如下所示:
public ICommand ListSelectionChangedCommand
{
get
{
if (_ListSelectionChangedCommand == null)
{
_ListSelectionChangedCommand = new RelayCommand<object>((p) =>
{
var spara = p as string;
if (spara == null)
return;
foreach (var item in List2) // Evaluate selected string in left List1
{ // against each item in List2
item.MatchString(spara);
}
});
}
return _ListSelectionChangedCommand;
}
}
Public ReadOnly Property ListSelectionChangedCommand As ICommand
Get
If _ListSelectionChangedCommand Is Nothing Then
_ListSelectionChangedCommand = New RelayCommand(Of Object)(
Sub(p)
Dim spara = TryCast(p, String)
If spara Is Nothing Then Return
For Each item In List2 '' Evaluate selected string in left List1
item.MatchString(spara) '' against each item in List2
Next
End Sub)
End If
Return _ListSelectionChangedCommand
End Get
End Property
spara
变量包含代表左侧 ListBox
(List1
)中选定项目的 string
。foreach
循环扫描右侧 ListBox
(List2
)中的每个项目,并调整 StringMatchItem
类中的匹配指示器属性 Range
属性。
public bool MatchString(string searchString)
{
if (string.IsNullOrEmpty(DisplayString) == true &&
string.IsNullOrEmpty(searchString) == true)
{
Range = new SelectionRange(0,0);
return true;
}
else
{
if (string.IsNullOrEmpty(DisplayString) == true ||
string.IsNullOrEmpty(searchString) == true)
{
Range = new SelectionRange(-1, -1);
return false;
}
}
int start;
if ((start = DisplayString.IndexOf(searchString)) >= 0)
{
Range = new SelectionRange(start, start + searchString.Length);
return true;
}
else
{
Range = new SelectionRange(start, -1);
return false;
}
}
Public Function MatchString(ByVal searchString As String) As Boolean
If String.IsNullOrEmpty(DisplayString) = True AndAlso String.IsNullOrEmpty(searchString) = True Then
Range = New SelectionRange(0, 0)
Return True
Else
If String.IsNullOrEmpty(DisplayString) = True OrElse String.IsNullOrEmpty(searchString) = True Then
Range = New SelectionRange(-1, -1)
Return False
End If
End If
Dim start As Integer
start = DisplayString.IndexOf(searchString)
If start >= 0 Then
Range = New SelectionRange(start, start + searchString.Length)
Return True
Else
Range = New SelectionRange(start, -1)
Return False
End If
End Function
右侧 ListBox
(List2
)中的所有项目都是 StringMatchItem
类型的对象。这个类包含一个 Range
属性,它遵循 ISelectionRange
接口定义。右侧 ListBox
中每个项目的 TextBlock
都绑定到 StringMatchItem
类中每个 viewmodel
项目的 Range
属性,如下所示:
<ListBox Grid.Column="1" Grid.Row="1"
ItemsSource="{Binding List2}"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Margin="3">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayString}"
behav:HighlightTextBlockBehavior.Range="{Binding Range}"
/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
HighlightTextBlockBehavior
行为进而监听 Range
属性的变化,并对这些变化做出反应,高亮显示我们希望特别关注的区域。
private static void OnRangeChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
TextBlock txtblock = d as TextBlock;
if (txtblock == null)
return;
var range = GetRange(d); // Get the bound Range value to do highlighting
// Standard background is transparent
SolidColorBrush normalBackGround = new SolidColorBrush(Color.FromArgb(00, 00, 00, 00));
if (range != null)
{
if (range.NormalBackground != default(Color))
normalBackGround = new SolidColorBrush(range.NormalBackground);
}
var txtrange = new TextRange(txtblock.ContentStart, txtblock.ContentEnd);
txtrange.ApplyPropertyValue(TextElement.BackgroundProperty, normalBackGround);
if (range == null)
return;
if (range.Start < 0 || range.End < 0) // Nothing to highlight here :-(
return;
try
{
Color selColor = (range.DarkSkin ? Color.FromArgb(255, 254, 252, 200) :
Color.FromArgb(255, 208, 247, 255));
Brush selectionBackground = new SolidColorBrush(selColor);
if (range != null)
{
if (range.SelectionBackground != default(Color))
selectionBackground = new SolidColorBrush(range.SelectionBackground);
}
TextRange txtrangel = new TextRange(
txtblock.ContentStart.GetPositionAtOffset(range.Start + 1)
, txtblock.ContentStart.GetPositionAtOffset(range.End + 1));
txtrangel.ApplyPropertyValue(TextElement.BackgroundProperty, selectionBackground);
}
catch (Exception exc)
{
Console.WriteLine(exc.Message);
Console.WriteLine(exc.StackTrace);
}
}
Private Sub OnRangeChanged(ByVal d As DependencyObject,
ByVal e As DependencyPropertyChangedEventArgs)
Dim txtblock As TextBlock = TryCast(d, TextBlock)
If txtblock Is Nothing Then Return
Dim range = GetRange(d) '' Get the bound Range value to do highlighting
Dim normalBackGround As SolidColorBrush = New SolidColorBrush(Color.FromArgb(0, 0, 0, 0))
If range IsNot Nothing Then
If range.NormalBackground <> Nothing Then normalBackGround = New SolidColorBrush(range.NormalBackground)
End If
Dim txtrange = New TextRange(txtblock.ContentStart, txtblock.ContentEnd)
txtrange.ApplyPropertyValue(TextElement.BackgroundProperty, normalBackGround)
If range Is Nothing Then Return
If range.Start < 0 OrElse range.[End] < 0 Then Return
Try
Dim selColor As Color = (If(range.DarkSkin, Color.FromArgb(255, 254, 252, 200),
Color.FromArgb(255, 208, 247, 255)))
Dim selectionBackground As Brush = New SolidColorBrush(selColor)
If range IsNot Nothing Then
If range.SelectionBackground <> Nothing Then selectionBackground = New SolidColorBrush(range.SelectionBackground)
End If
Dim txtrangel As TextRange = New TextRange(txtblock.ContentStart.GetPositionAtOffset(range.Start + 1), txtblock.ContentStart.GetPositionAtOffset(range.[End] + 1))
txtrangel.ApplyPropertyValue(TextElement.BackgroundProperty, selectionBackground)
Catch exc As Exception
Console.WriteLine(exc.Message)
Console.WriteLine(exc.StackTrace)
End Try
End Sub
我们在这里可以看到,该行为使用了 WPF 的 TextRange 类,它通常用于在 WPF FlowDocument
中选择文本,但在这里也可以使用,因为 WPF TextBlock
也提供了所需的属性和方法。
结论
重用代码不应需要更多,只需
HighlightTextBlockBehavior
行为ISelectionRange
接口,以及- 实现该接口的
viewmodel
类SelectionRange
这 3 项的应用应该很简单灵活。事实证明,我们还可以使用该接口在后台线程中计算匹配范围,而不会在绑定到 UI 集合项时遇到其他问题,正如我们将在单独的奖励内容 [1] 部分中看到的。
目前,我不确定这 3 项是否应该包含在额外的 Nuget 包库中,但我会考虑一下。我很想就这个问题以及这里开发的 Code Project 获得一些反馈。
论坛(Forum below)中有一位读者建议以不同的方式实现这一点 - 主要是在附加行为中进行字符串匹配,而不是在 viewmodel 中。 Meshack Musundi 沿着这条思路,在这里实现了 替代解决方案。目前我不确定哪个解决方案更好,我猜取决于 :-) 我总是喜欢在 viewmodel 中进行数据处理(字符串匹配),让视图关心显示,但是有不同的方法可以实现这一点,这里有 2 种有趣的替代方案供您选择。