不应该在视图模型类中实现此类与视图相关的逻辑。滚动逻辑必须是控件的一部分。
此外,Run
是一个纯视图类。它继承自 FrameworkElement
,这应该提示您尽可能避免在视图模型中处理此 UI 元素。
以下代码片段会在文本更改时将 RichTextBox
文档滚动到底部:
<RichTextBox TextChanged="OnTextChanged" />
private void OnTextChanged(object sender, TextChangedEventArgs e)
=> this.Dispatcher.InvokeAsync((sender as RichTextBox).ScrollToEnd, DispatcherPriority.Background);
由于你正在实现一个简单的消息视图,所以
RichTextBox
不是正确的控件,
TextBlock
更适合(它还支持像
Run
这样的
Inline
元素来着色文本)。现在,由于你想显示多行文本,所以应该基于
ListBox
来实现视图,并借助
TextBlock
来呈现其项。这种方法的主要优点是性能极佳。如果要显示大量的消息,则
ListBox
提供了 UI 虚拟化功能 - 它总是可以平滑地滚动。而沉重的
RichTextBox
会变得很快缓慢。
由于你的视图模型只需要处理数据,首先要引入一个数据模型,例如
LogMessage
和它的相关类型:
LogMessage.cs
public class LogMessage : INotifyPropertyChanged
{
public LogMessage(string message, LogLevel logLevel)
{
this.Message = message;
this.LogLevel = logLevel;
}
public string Message { get; }
public LogLevel LogLevel { get; }
public bool IsNewLine { get; init; }
public event PropertyChangedEventHandler PropertyChanged;
}
LogLevel.cs
public enum LogLevel
{
Default = 0,
Debug,
Info
}
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<LogMessage> LogMessages { get; }
public event PropertyChangedEventHandler PropertyChanged;
public MainViewModel()
{
this.LogMessages = new ObservableCollection<LogMessage>();
WriteLine("Debug test message.", LogLevel.Debug);
WriteLine("Info test message.", LogLevel.Info);
}
public void WriteLine(string message, LogLevel logLevel)
{
var newMessage = new LogMessage(message, logLevel) { IsNewLine = true };
this.LogMessages.Add(newMessage);
}
}
然后实现显示消息的视图。虽然这个例子使用了一个 UserControl
,但我强烈建议通过扩展 Control
来创建自定义控件:
LogLevelToBrushConverter.cs
public class LogLevelToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value switch
{
LogLevel.Debug => Brushes.Blue,
LogLevel.Info => Brushes.Gray,
_ => Brushes.Black
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
LogMessageBox.xaml.cs
public partial class LogMessageBox : UserControl
{
public IList<object> LogMessagesSource
{
get => (IList<object>)GetValue(LogMessagesSourceProperty);
set => SetValue(LogMessagesSourceProperty, value);
}
public static readonly DependencyProperty LogMessagesSourceProperty = DependencyProperty.Register(
"LogMessagesSource",
typeof(IList<object>),
typeof(LogMessageBox),
new PropertyMetadata(default(IList<object>), OnLogMessagesSourceChanged));
public LogMessageBox()
{
InitializeComponent();
}
private static void OnLogMessagesSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> (d as LogMessageBox).OnLogMessagesSourceChanged(e.OldValue as IList<object>, e.NewValue as IList<object>);
protected virtual void OnLogMessagesSourceChanged(IList<object> oldMessages, IList<object> newMessages)
{
if (oldMessages is INotifyCollectionChanged oldObservableCollection)
{
oldObservableCollection.CollectionChanged -= OnLogMessageCollectionChanged;
}
if (newMessages is INotifyCollectionChanged newObservableCollection)
{
newObservableCollection.CollectionChanged += OnLogMessageCollectionChanged;
}
}
private void OnLogMessageCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
object lastMessageItem = this.LogMessagesSource.LastOrDefault();
ListBox listBox = this.Output;
Dispatcher.InvokeAsync(
() => listBox.ScrollIntoView(lastMessageItem),
DispatcherPriority.Background);
}
}
LogMessageBox.xaml
<UserControl>
<ListBox x:Name="Output"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=LogMessagesSource}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsHitTestVisible"
Value="False" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</UserControl>
使用示例
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<local:LogLevelToBrushConverter x:Key="LogLevelToBrushConverter" />
<DataTemplate DataType="{x:Type local:LogMessage}">
<TextBlock Text="{Binding Message, Mode=OneTime}"
Foreground="{Binding LogLevel, Mode=OneTime, Converter={StaticResource LogLevelToBrushConverter}}" />
</DataTemplate>
</Window.Resources>
<LogMessageBox LogMessagesSource="{Binding LogMessages}" />
</Window>
IValueConverter.Convert()
实现中添加了一些代码,将返回的Brush
颜色名称写入文件。 它确实按预期工作。 或许是某种 XAML 绑定问题? - Mike BrunoLogMessage
公开只读属性,因此不会引发PropertyChanged
事件,这对于允许动态数据是至关重要的。此外,与LogMessage
的数据绑定配置为OneTime
。我只是说,以防您的问题与在将项目添加到源集合后动态更新数据有关。 - BionicCode