Home > other >  WPF MVVM Binding of itemssource to an ObservableCollection inside a different class and add items to
WPF MVVM Binding of itemssource to an ObservableCollection inside a different class and add items to

Time:03-24

I have integrated a small application with a console window that passes information to the user.

In meiner view habe ich ein ItemControl und im Template ein TextBlock, welches den "ConsoleText" anzeigt in der gewünschten Farbe "FontColour":

<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>

Mein CodeBehind beinhaltet lediglich den DataContext:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainWindowViewModel();
    }
}

My ViewMode for the view has three information:

  1. Instantiate MyLogicClass, which is structured in another project and which is given as reference.
  2. ConsoleOutputList is instantiated to be able to display "ConsoleOutput" in the view later on.
  3. The button is not displayed in the view because it is not relevant in the view and it works without any problems.

And here the 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 contains an ObservableCollection from "ConsoleLine" (also in another project). The "ConsoleLine" may have a string and a SolidColorBrush in the ctor (I think this one is not relevant either).

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));
    }

}

This classe is used for all application logic (also in another project). As a test I have here the method Test() to simply add a text.

public class MyLogicClass
{
    ConsoleOutputList Logger = new ConsoleOutputList();
    
    public void Test()
    {
        Logger.WriteToConsole($"Test", Brushes.Gray);
    }

}

Now I have two problems:

  1. I add a new element in ctor of ConsoleOutputList as a test to see in general if my view works correctly => But does not work
  2. I have the Test() method to simply test adding new items to the ObservableCollection to see if they show up after being added => But of course also does not work

And yes I know - I create two instances of ConsoleOutputList and this is not correct (this is the reason for the second problem). But I don`t know how to do it better because I need to access the WriteToConsole() from everywhere in the code. (Maybe change to static?) But how do I solve then the first problem and how does it then works with static property and showing them up in the view.

Update: Even if I change everything to static the "Test" line is shown in green but everything which I add afterwards is not shown in the GUI: Visual Studio

CodePudding user response:

In WPF you can't bind to methods or fields. You must bind to a public property (see Microsoft Docs: Binding source types to learn more about the supported binding sources).

To fix issue 1) you must implement the MainWindowViewModel.logger field as public property. If the property is expected to change, it must raise the INotifyPropertyChanged.PropertyChanged event.

To fix 2), you must distribute a shared instance of ConsoleOutputList throughout your application. To expose it as a static instance is one solution, but generally not recommended. The better solution is to pass a shared instance to the constructor of the each type that depends on ConsoleOutputList.

The fixed solution could look as follows:

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();
  }
}

Also don't wrap the ItemsControl into a ScrollViewer. Use a ListBox instead. ListBox is an enhanced ItemsControl, that has a ScrollViewer and UI virtualization enabled by default. If you expect to generate many log entries you will end up with many items in the list. If you don't use UI virtualization, your ItemsControl will kill the performance/responsiveness of your GUI.

<DockPanel>
  <ListBox ItemsSource="{Binding Logger.ConsoleOutput}">
    <ListBox.ItemTemplate>
      ...
    </ListBox.ItemTemplate>
  </ListBox>
</DockPanel>

To allow updating the collection ConsoleOutput from a background thread or a non UI thread in general, you can either use the Dispatcher to update the collection (not recommended in this case):

Dispatcher.InvokeAsync(() => myCollection.Add(item));

or configure the binding engine to marshal the collection's CollectionChanged event to the dispatcher thread.
Since the critical object is a collection that serves as a binding source, I recommend to configure the binding engine by calling the static BindingOperations.EnableCollectionSynchronization method. It is essential that the method is invoked on the dispatcher thread:

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);
  }
}
  • Related