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 有三个信息:

  1. 实例化 MyLogicClass,它在另一个项目中构建,作为参考。
  2. ConsoleOutputList 已实例化,以便稍后能够在视图中显示“ConsoleOutput”。
  3. 该按钮未显示在视图中,因为它在视图中不相关,并且可以正常工作。

这里是 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);
    }

}

现在我有两个问题:

  1. 我在 ConsoleOutputList 的 ctor 中添加了一个新元素作为测试,以查看我的视图是否正常工作 => 但是不起作用
  2. 我有 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。请改用 ListBoxListBox 是增强的 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);
  }
}