WPF MVVM 将 itemssource 绑定到不同 class 内的 ObservableCollection 并向其添加项目
WPF MVVM Binding of itemssource to an ObservableCollection inside a different class and add items to it
我已经将一个小型应用程序与控制台 window 集成在一起,将信息传递给用户。
在我的视图中,我在 ItemControl 和 im 模板中使用了 TextBlock,在 der gewünschten Farbe“FontColour”中添加了“ConsoleText”:
<Window x:Class="MyApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helper="clr-namespace:MyApplication.Helpers"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" MinHeight="600" MinWidth="800">
<Grid>
<DockPanel Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" Grid.RowSpan="4" Background="Black">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" helper:ScrollViewerExtension.AutoScroll="True">
<ItemsControl ItemsSource="{Binding logger.ConsoleOutput}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ConsoleText}" Foreground="{Binding FontColour}" FontSize="14"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
</Window>
我的 CodeBehind beinhaltet lediglich den DataContext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
我的视图的 ViewMode 有三个信息:
- 实例化 MyLogicClass,它在另一个项目中构建,作为参考。
- ConsoleOutputList 已实例化,以便稍后能够在视图中显示“ConsoleOutput”。
- 该按钮未显示在视图中,因为它在视图中不相关,并且可以正常工作。
这里是 ViewModel:
public class MainWindowViewModel : ObservableObject
{
MyLogicClass myLogicClass = new MyLogicClass(null);
ConsoleOutputList logger = new ConsoleOutputList();
public MainWindowViewModel()
{
MyButtonCommand = new RelayCommand(o => Task.Run(() => myLogicClass.Test()));
}
public ICommand MyButtonCommand { get; }
}
ConsoleOutputList 包含来自“ConsoleLine”(也在另一个项目中)的 ObservableCollection。
“ConsoleLine”在构造函数中可能有一个字符串和一个 SolidColorBrush(我认为这也不相关)。
public class ConsoleOutputList : ObservableObject
{
public ConsoleOutputList()
{
ConsoleOutput = new ObservableCollection<ConsoleLine>();
// For testing purposes I add a random entry to see if the binding in general works - but this doesn`t work neither
ConsoleOutput.Add(new ConsoleLine("Test", Brushes.Green));
}
public ObservableCollection<ConsoleLine> consoleOutput { get; set; }
public ObservableCollection<ConsoleLine> ConsoleOutput
{
get
{
return consoleOutput;
}
set
{
consoleOutput = value;
}
}
//Used to add new lines to the ObservableCollection
public void WriteToConsole(object msg, SolidColorBrush fontColour)
{
ConsoleOutput.Add(new ConsoleLine(msg, fontColour));
}
}
该类用于所有应用程序逻辑(也在另一个项目中)。
作为测试,我在这里使用方法 Test() 来简单地添加文本。
public class MyLogicClass
{
ConsoleOutputList Logger = new ConsoleOutputList();
public void Test()
{
Logger.WriteToConsole($"Test", Brushes.Gray);
}
}
现在我有两个问题:
- 我在 ConsoleOutputList 的 ctor 中添加了一个新元素作为测试,以查看我的视图是否正常工作 => 但是不起作用
- 我有 Test() 方法来简单地测试将新项目添加到 ObservableCollection 以查看它们在添加后是否显示 => 但当然也不起作用
是的,我知道 - 我创建了两个 ConsoleOutputList 实例,这是不正确的(这是第二个问题的原因)。
但我不知道如何做得更好,因为我需要从代码中的任何地方访问 WriteToConsole()。 (也许改为静态?)
但是我该如何解决第一个问题以及它如何与静态 属性 一起工作并将它们显示在视图中。
更新:
即使我将所有内容都更改为静态,“测试”行也会显示为绿色,但我之后添加的所有内容都不会显示在 GUI 中:
Visual Studio
在 WPF 中,您无法绑定到方法或字段。您必须绑定到 public 属性(请参阅 Microsoft Docs: Binding source types 以了解有关支持的绑定源的更多信息)。
要解决问题 1),您必须将 MainWindowViewModel.logger
字段实现为 public 属性。如果 属性 需要改变,它必须引发 INotifyPropertyChanged.PropertyChanged
事件。
要修复 2),您必须在整个应用程序中分发 ConsoleOutputList
的共享实例。将其公开为静态实例是一种解决方案,但通常不推荐。更好的解决方案是将共享实例传递给依赖于 ConsoleOutputList
.
的每种类型的构造函数
固定的解决方案如下所示:
MainWindowViewModel.cs
public class MainWindowViewModel : ObservableObject
{
public MainWindowViewModel(ConsoleOutputList logger, MyLogicClass myLogicClass)
{
this.Logger = logger;
this.MyLogicClass = myLogicClass;
this.MyButtonCommand = new RelayCommand(o => Task.Run(() => myLogicClass.Test()));
}
public ConsoleOutputList Logger { get; }
public ICommand MyButtonCommand { get; }
private MyLogicClass MyLogicClass { get; }
}
MyLogicClass.cs
public class MyLogicClass
{
private ConsoleOutputList Logger { get; }
public MyLogicClass(ConsoleOutputList logger) => this.Logger = logger;
public void Test()
{
this.Logger.WriteToConsole($"Test", Brushes.Gray);
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel mainWindowViewModel)
{
InitializeComponent();
this.DataContext = mainWindowViewModel;
}
}
App.xaml
<Application Startup="App_OnStartup">
</Application>
App.xaml.cs
public partial class App : Application
{
private void App_OnStartup(object sender, StartupEventArgs e)
{
/** Initialize the application with the shared instance **/
var sharedLoggerInstance = new ConsoleOutputList();
var classThatNeedsLogger = new MyLogicClass(sharedLoggerInstance);
var mainViewModel = new MainWindowViewModel(sharedLoggerInstance, classThatNeedsLogger);
var mainWindow = new MainWindow(mainViewModel);
mainWindow.Show();
}
}
也不要将 ItemsControl
换成 ScrollViewer
。请改用 ListBox
。 ListBox
是增强的 ItemsControl
,默认启用 ScrollViewer
和 UI 虚拟化。如果您希望生成许多日志条目,您最终会在列表中看到许多项目。如果您不使用 UI 虚拟化,您的 ItemsControl
将杀死您的 GUI.
的 performance/responsiveness
<DockPanel>
<ListBox ItemsSource="{Binding Logger.ConsoleOutput}">
<ListBox.ItemTemplate>
...
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
要允许从后台线程或一般的非 UI 线程更新集合 ConsoleOutput
,您可以使用 Dispatcher
更新集合(不推荐在此案例):
Dispatcher.InvokeAsync(() => myCollection.Add(item));
或配置绑定引擎以将集合的 CollectionChanged
事件编组到调度程序线程。
由于关键对象是作为绑定源的集合,我建议通过调用静态BindingOperations.EnableCollectionSynchronization
方法来配置绑定引擎。必须在调度程序线程上调用该方法:
ConsoleOutputList.cs
public class ConsoleOutputList : ObservableObject
{
private object SyncLock { get; }
public ConsoleOutputList()
{
this.SyncLock = new object();
this.ConsoleOutput = new ObservableCollection<ConsoleLine>();
// Configure the binding engine to marshal the CollectionChanged event
// of this collection to the UI thread to prevent cross-thread exceptions
BindingOperations.EnableCollectionSynchronization(this.ConsoleOutput, this.SyncLock);
}
}
我已经将一个小型应用程序与控制台 window 集成在一起,将信息传递给用户。
在我的视图中,我在 ItemControl 和 im 模板中使用了 TextBlock,在 der gewünschten Farbe“FontColour”中添加了“ConsoleText”:
<Window x:Class="MyApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helper="clr-namespace:MyApplication.Helpers"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="800" MinHeight="600" MinWidth="800">
<Grid>
<DockPanel Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="3" Grid.RowSpan="4" Background="Black">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" helper:ScrollViewerExtension.AutoScroll="True">
<ItemsControl ItemsSource="{Binding logger.ConsoleOutput}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ConsoleText}" Foreground="{Binding FontColour}" FontSize="14"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
</Window>
我的 CodeBehind beinhaltet lediglich den DataContext:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
我的视图的 ViewMode 有三个信息:
- 实例化 MyLogicClass,它在另一个项目中构建,作为参考。
- ConsoleOutputList 已实例化,以便稍后能够在视图中显示“ConsoleOutput”。
- 该按钮未显示在视图中,因为它在视图中不相关,并且可以正常工作。
这里是 ViewModel:
public class MainWindowViewModel : ObservableObject
{
MyLogicClass myLogicClass = new MyLogicClass(null);
ConsoleOutputList logger = new ConsoleOutputList();
public MainWindowViewModel()
{
MyButtonCommand = new RelayCommand(o => Task.Run(() => myLogicClass.Test()));
}
public ICommand MyButtonCommand { get; }
}
ConsoleOutputList 包含来自“ConsoleLine”(也在另一个项目中)的 ObservableCollection。 “ConsoleLine”在构造函数中可能有一个字符串和一个 SolidColorBrush(我认为这也不相关)。
public class ConsoleOutputList : ObservableObject
{
public ConsoleOutputList()
{
ConsoleOutput = new ObservableCollection<ConsoleLine>();
// For testing purposes I add a random entry to see if the binding in general works - but this doesn`t work neither
ConsoleOutput.Add(new ConsoleLine("Test", Brushes.Green));
}
public ObservableCollection<ConsoleLine> consoleOutput { get; set; }
public ObservableCollection<ConsoleLine> ConsoleOutput
{
get
{
return consoleOutput;
}
set
{
consoleOutput = value;
}
}
//Used to add new lines to the ObservableCollection
public void WriteToConsole(object msg, SolidColorBrush fontColour)
{
ConsoleOutput.Add(new ConsoleLine(msg, fontColour));
}
}
该类用于所有应用程序逻辑(也在另一个项目中)。 作为测试,我在这里使用方法 Test() 来简单地添加文本。
public class MyLogicClass
{
ConsoleOutputList Logger = new ConsoleOutputList();
public void Test()
{
Logger.WriteToConsole($"Test", Brushes.Gray);
}
}
现在我有两个问题:
- 我在 ConsoleOutputList 的 ctor 中添加了一个新元素作为测试,以查看我的视图是否正常工作 => 但是不起作用
- 我有 Test() 方法来简单地测试将新项目添加到 ObservableCollection 以查看它们在添加后是否显示 => 但当然也不起作用
是的,我知道 - 我创建了两个 ConsoleOutputList 实例,这是不正确的(这是第二个问题的原因)。 但我不知道如何做得更好,因为我需要从代码中的任何地方访问 WriteToConsole()。 (也许改为静态?) 但是我该如何解决第一个问题以及它如何与静态 属性 一起工作并将它们显示在视图中。
更新: 即使我将所有内容都更改为静态,“测试”行也会显示为绿色,但我之后添加的所有内容都不会显示在 GUI 中: Visual Studio
在 WPF 中,您无法绑定到方法或字段。您必须绑定到 public 属性(请参阅 Microsoft Docs: Binding source types 以了解有关支持的绑定源的更多信息)。
要解决问题 1),您必须将 MainWindowViewModel.logger
字段实现为 public 属性。如果 属性 需要改变,它必须引发 INotifyPropertyChanged.PropertyChanged
事件。
要修复 2),您必须在整个应用程序中分发 ConsoleOutputList
的共享实例。将其公开为静态实例是一种解决方案,但通常不推荐。更好的解决方案是将共享实例传递给依赖于 ConsoleOutputList
.
固定的解决方案如下所示:
MainWindowViewModel.cs
public class MainWindowViewModel : ObservableObject
{
public MainWindowViewModel(ConsoleOutputList logger, MyLogicClass myLogicClass)
{
this.Logger = logger;
this.MyLogicClass = myLogicClass;
this.MyButtonCommand = new RelayCommand(o => Task.Run(() => myLogicClass.Test()));
}
public ConsoleOutputList Logger { get; }
public ICommand MyButtonCommand { get; }
private MyLogicClass MyLogicClass { get; }
}
MyLogicClass.cs
public class MyLogicClass
{
private ConsoleOutputList Logger { get; }
public MyLogicClass(ConsoleOutputList logger) => this.Logger = logger;
public void Test()
{
this.Logger.WriteToConsole($"Test", Brushes.Gray);
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel mainWindowViewModel)
{
InitializeComponent();
this.DataContext = mainWindowViewModel;
}
}
App.xaml
<Application Startup="App_OnStartup">
</Application>
App.xaml.cs
public partial class App : Application
{
private void App_OnStartup(object sender, StartupEventArgs e)
{
/** Initialize the application with the shared instance **/
var sharedLoggerInstance = new ConsoleOutputList();
var classThatNeedsLogger = new MyLogicClass(sharedLoggerInstance);
var mainViewModel = new MainWindowViewModel(sharedLoggerInstance, classThatNeedsLogger);
var mainWindow = new MainWindow(mainViewModel);
mainWindow.Show();
}
}
也不要将 ItemsControl
换成 ScrollViewer
。请改用 ListBox
。 ListBox
是增强的 ItemsControl
,默认启用 ScrollViewer
和 UI 虚拟化。如果您希望生成许多日志条目,您最终会在列表中看到许多项目。如果您不使用 UI 虚拟化,您的 ItemsControl
将杀死您的 GUI.
<DockPanel>
<ListBox ItemsSource="{Binding Logger.ConsoleOutput}">
<ListBox.ItemTemplate>
...
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
要允许从后台线程或一般的非 UI 线程更新集合 ConsoleOutput
,您可以使用 Dispatcher
更新集合(不推荐在此案例):
Dispatcher.InvokeAsync(() => myCollection.Add(item));
或配置绑定引擎以将集合的 CollectionChanged
事件编组到调度程序线程。
由于关键对象是作为绑定源的集合,我建议通过调用静态BindingOperations.EnableCollectionSynchronization
方法来配置绑定引擎。必须在调度程序线程上调用该方法:
ConsoleOutputList.cs
public class ConsoleOutputList : ObservableObject
{
private object SyncLock { get; }
public ConsoleOutputList()
{
this.SyncLock = new object();
this.ConsoleOutput = new ObservableCollection<ConsoleLine>();
// Configure the binding engine to marshal the CollectionChanged event
// of this collection to the UI thread to prevent cross-thread exceptions
BindingOperations.EnableCollectionSynchronization(this.ConsoleOutput, this.SyncLock);
}
}