C
C#•2mo ago
Qwerz

Adding items to an observable collection from 'outside' (MVVM)

Hi all, I'm developing a desktop application in a small team of developers using WPF. I am responsible for the WPF part of the project while my coworkers are responsible for the core functionality. We are all new to the MVVM approach, and I'm afraid that I have not grasped it properly despite me having thought that I did. I'm going to simplify my problem a little for the sake of explaining it without too much detail. I created a view with an items control. The items control has an ItemsSource binding to an observable collection in the corresponding ViewModel. The goal is to display events (errors, success messages, etc.) in this view, hence the observable collection is one of "messages". The problem lies in adding events to the collection from across the program. The general structure of the project consists of multiple manager classes handling functions such as getting and setting data from a database, e.g. a "DatabaseManager". Ideally, I would have thought that in my ViewModel, I create a static function "NewEvent" which adds an event to the Observable collection. However, this isn't possible because the ViewModel and its collection are not static. I.e. : a function runs in the DatabaseManager and from there, a new event should be deployed and shown in the view via the observable collection. What is the correct way to implement this? I assume I could make the collection static and reference the VM directly, but is that the correct way WPF wants me to take? This is an example of a general problem I seem to be having with the MVVM methodology. I understand the View and ViewModel separation, however I'm utterly uncertain how to connect the UI Framework part with the actual functions in the backbone of any application. The vast majority of tutorials show how to bind a text box and a button to the extent that items are added to a list 'visually'. They never go into detail how to implement functions in the actual backend of an application. Thanks a lot in Adv.
7 Replies
SpReeD
SpReeD•2mo ago
I understand the View and ViewModel separation, however I'm utterly uncertain how to connect the UI Framework part with the actual functions in the backbone of any application. MVVM consists of more than only the two. Think of it that way, you got the View, the ViewModel and the Business Logic, three main parts. Also the fourth part is binding. The ViewModel is the mediator between your View and the BL, nothing more, nothing less, it's also bound a bit more towards the View than the Logic. The BL is the actual part that interacts with the data, might a database, might be a webcrawler, might be some kind of API endpoint. It does all the that and builts the backend, the engine and the engine compartment, if you want so. So, any logging and diagnostics also happens there, like an diagnosis of a car. The ViewModel provides the objects for the View, also it's responsible for interacting with the BL and getting or setting the correct data. It's like full of routines which sub-routines are in the BL. - Everytime an ObservableProperty (doesn't matter if it's the ObservableCollection or a simple string, or any primitive like an int, as long as the setter implements the OnPropertyChanged event) is set, it'll submit a signal to the view to redraw & refresh the bound control. The View is dumb af, it has all the fancy UI stuff, like graphics, animations, etc. and nothing more, no BL code, no nothing else. The only information it needs is the name of Property to bind to. Under the hood there's obviously more to it, but that's a short summary of the MVVM. Without any code sample it's kinda hard to help, but it sounds to me that you need to expand your VM like a lot. First, I hope you're using commands, the ICommand interface and not any events. Next up, with those commands, the CanExecute predicate. So that there's a way to change Visibility of UI elements, etc. Also, I encourage you to use a MVVM framework like the CommunityToolkit. It's very helpful, since WPF/MVVM has a lot of boilerplate code and some overhead in creation and handling datasources. At this point and as always, there's up and downs to everything, MVVM isn't the holy grale of design patterns. As always, everything depends on the use-case.
Qwerz
Qwerz•2mo ago
I think I'm slowly getting the hang of it. I have a (to my judgement) a decent MVVM backbone in my Application. My Navigation and many user interactive listings are MVVM conform, and I'm using commands wherever I can. However... whenever I leave my View or ViewModel and venture into spaces worked on by my coworkers (who do not know the MVVM pattern) I run into the problem of calling methods inside my ViewModel. Weird thing is, I truly understand why I can't just call a non-static function of my ViewModel from a non-static manager context- but I just don't know how do handle this situation. I will provide a small example below:
c#
//ViewModel snippet
private ObservableCollection<EventItem> _events;
public MainViewModel()
{
_events = new ObservableCollection<EventItem>();
_events.Add(new EventItem("hello world!!"));
}
public void NewEvent(EventItem eventItem)
{
_events.Add(eventItem);
//more handling like timer, event type and more (not relevant)
}

//DatabaseManager snippet
private static void CreateDb(string s)
{
SQLConnection.CreateFile(s);
LocalDb = new SQLConnection(connectionDataSourceConstant);
LocalDb.Open();

//Create new Event here that should be entered into ObservableCollection in MainViewModel
EventManager.CreateEvent("Created database");
}

//EventManager snippet
public static void CreateEvent(string message)
{
newEvent = new EventItem(message);
MainViewModel.NewEvent(newEvent);
}
c#
//ViewModel snippet
private ObservableCollection<EventItem> _events;
public MainViewModel()
{
_events = new ObservableCollection<EventItem>();
_events.Add(new EventItem("hello world!!"));
}
public void NewEvent(EventItem eventItem)
{
_events.Add(eventItem);
//more handling like timer, event type and more (not relevant)
}

//DatabaseManager snippet
private static void CreateDb(string s)
{
SQLConnection.CreateFile(s);
LocalDb = new SQLConnection(connectionDataSourceConstant);
LocalDb.Open();

//Create new Event here that should be entered into ObservableCollection in MainViewModel
EventManager.CreateEvent("Created database");
}

//EventManager snippet
public static void CreateEvent(string message)
{
newEvent = new EventItem(message);
MainViewModel.NewEvent(newEvent);
}
I hope this helps to understand what I would ideally like to accomplish: present my coworkers an easy to implement one-liner to use in their respective areas; just like in the example. I really struggle with this. Just like you said, the BusinessLogic comes behind the ViewModel, but I find it extremely difficult and counter intuitive to communicate between the two. Do I have to teach all my coworkers the MVVM pattern and ensurre every class is written specifically to fit the pattern? Thank you!
SpReeD
SpReeD•2mo ago
I read your text and I got to comment before I go on and check the code. Yes, please, for the love of god, you gotta teach'em MVVM and everything - I was in a similar situation in my previous job, it was straight hell. Everything my coworkers did wasn't usable, no encapsulation, not proper usage of basic OOP rules, etc. - So I ended up doing all the work and I couldn't outsource anything because of that. Training courses are a great way to learn, no, please no linked-in learning or YouTube vids - real training courses with real tutors. Lucky I was deputy depart. leader back then, so I was responsible for the organization and stuff - but I see no way around, people need to learn and sometimes, coworkers, have to learn, otherwise they cannot work in a team. I admit, my view on this might be a bit harsh, but I was in that situation for more than four years, I'm pre-damaged 😄
Qwerz
Qwerz•2mo ago
Oh I can relate...😅 Without spilling too much salt; I also find it quite difficult to outsource features in the team. My favourite: new views are not implemented into the MVVM navigation, but are opened using dialogs -.- . I'm having a tough time to pass anything UI related onto one of my coworkers, because what's produced rarely fits the projects structure. Anyways, I can't do everything on my own, and vast parts of the BL are unbeknown to me. I'm also not really in a leading position, but I'm charged to oversee the GUI aspect and UX decisions. Hence my preference to produce a static function. In the mid-term I plan on suggesting everyone (myself included) take an extensive course, but right now we have to focus on getting the work done.
SpReeD
SpReeD•2mo ago
public partial class EventItem : ObservableObject, IValidatableObject
{
[ObservableProperty]
private int id;
[ObservableProperty]
private string name;

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
List<ValidationResult> results = [];

if (this.Id == default)
{
results.Add(new ValidationResult("Id must be set", [nameof(this.Id)]));
}

if (string.IsNullOrEmpty(this.Name))
{
results.Add(new ValidationResult("Name cannot be empty", [ nameof(this.Name) ]));
}

// Add more validation rules as needed

return results;
}

public bool IsValid()
{
return !this.Validate(new(this)).Any();
}
}

//ViewModel snippet
[ObservableProperty]
private BindingList<EventItem> events;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddEventCommand))]
private EventItem theItemTheViewIsWorkingOn;

public MainViewModel() //ctor
{
this.Events = new([ new() { Id = 1, Name = "Foobar" } ]);
}

private bool CanExecuteAddEvent()
{
return this.TheItemTheViewIsWorkingOn.IsValid();
}

[RelayCommand(CanExecute = nameof(this.CanExecuteAddEvent))]
private void AddEvent()
{
this.Events.Add(this.TheItemTheViewIsWorkingOn); //run routines in BL and refresh the VM object
this.TheItemTheViewIsWorkingOn = null; //End View/Dialog/Modal - Window.Close();
}

//XAML on Button
Command="{Binding AddEventCommand}"
public partial class EventItem : ObservableObject, IValidatableObject
{
[ObservableProperty]
private int id;
[ObservableProperty]
private string name;

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
List<ValidationResult> results = [];

if (this.Id == default)
{
results.Add(new ValidationResult("Id must be set", [nameof(this.Id)]));
}

if (string.IsNullOrEmpty(this.Name))
{
results.Add(new ValidationResult("Name cannot be empty", [ nameof(this.Name) ]));
}

// Add more validation rules as needed

return results;
}

public bool IsValid()
{
return !this.Validate(new(this)).Any();
}
}

//ViewModel snippet
[ObservableProperty]
private BindingList<EventItem> events;

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(AddEventCommand))]
private EventItem theItemTheViewIsWorkingOn;

public MainViewModel() //ctor
{
this.Events = new([ new() { Id = 1, Name = "Foobar" } ]);
}

private bool CanExecuteAddEvent()
{
return this.TheItemTheViewIsWorkingOn.IsValid();
}

[RelayCommand(CanExecute = nameof(this.CanExecuteAddEvent))]
private void AddEvent()
{
this.Events.Add(this.TheItemTheViewIsWorkingOn); //run routines in BL and refresh the VM object
this.TheItemTheViewIsWorkingOn = null; //End View/Dialog/Modal - Window.Close();
}

//XAML on Button
Command="{Binding AddEventCommand}"
I haven't tested this code, but it should work, or at least give a glimpse on how it's supposed to work. In my example I've used the CommunityToolkit, but it's also possible without it, just more boilerplate code ... However, I've changed the ObservableCollection to BindingList, hence BindingList implements IBindingList and relays the INotifyPropertyChanged event from it's items, that's something an ObservableCollection isn't capable of, afaik (different in Avalonia and MAUI, as far as I remember). I also altered the model EventItem to make use of INotifyPropertyChanged in the GUI - usually models coming from the backend/BL do not implement this - so either you make a wrapper object or work around that by using base.OnPropertyChanged(this.Events); everytime you update an item inside it. You could run a test by adding a combobox in XAML like
<ComboBox ItemsSource="{Binding Events, Mode=OneWay}" SelectedItem="{Binding EventSelection}" DisplayMemberPath="Name"/>
<ComboBox ItemsSource="{Binding Events, Mode=OneWay}" SelectedItem="{Binding EventSelection}" DisplayMemberPath="Name"/>
Qwerz
Qwerz•2mo ago
Thank you so much for taking the time to help me out, I really really appreciate it! It will take me some time to test this out and work around community tool kit - I will get back as soon as I have come around to doing that. Alright, I get the approach but I'm still not sure how to call this from the BL in a static context? I see you're using a relay command, but that doesn't help me when I want to add an event from an otherwise completely unrelated class, right? Could you please include an example of how I would call the AddEvent() function from "somewhere" else?
SpReeD
SpReeD•2mo ago
There are a couple of approaches to this, some are considered best-practice, some are okay, and some are no way. All in all, you always want to avoid static, not only in this case, but in all cases in OOP, avoid it as much as possible. In MVVM the BL shouldn't call anything in the VM, the VM is the mediator, therefore the only case where you need to call a method from outside one VM, is from another VM, it's the same layer, so kind of okay'ish. My approach, not using a static, is to get the View/Window, it's DataContext instance and call the method. I usually use a property Instance of type Window in my VM, this one is set in the ctor of the View, after InitializeComponent, like ((MyWindowViewModel)this.DataContext).Instance = this;, this way I have the current instance of the View in the VM, no need to pass parameters here. On the other side, if I have the View, I also have access to the VM through (MyWindowViewModel)MainWindowInstance.DataContext. So either way, I can call interlayer methods. - When it's about the MainWindow, WPF has a built-in property for this Application.Current.MainWindow, this will get you the instance of the Window that's created on application startup/by startup-uri - but, use with caution, in other frameworks like Avalonia and MAUI it's different. All in all, there are objects you need to access and manipulate throughout the runtime, e.g. application configuration/user config. For this objects I do, what I call Runtimes.cs or RuntimeObjects.cs or just Globals.cs - this is a static class with all the static properties which are set throughout the runtime. In the case of an user-config internal static ConfigurationManager<ConfigurationModel> { get; set; } , it's loaded somewhere in the beginning, set on runtime and saved/serialized (JSON) there. But I advise to use this only for BL, don't make a static View or a static VM - if you use Page's, you could cache those in a static List<Page> or something to keep the entered and selected values of textboxes etc. - Here, you need to write a Pagemanager class. For instance.