Introduction
I have an application that imports lab instrument data while it is running. This data is imported and then displayed in a ListView
at an interval set by the end-user as per his or her testing requirements. When a value of interest appears in this ListView
that they watch, they then press a Start button and the application begins performing calculations on that datum and subsequent data until a Stop button is pressed. So on the left side of the screen is a View for displaying the imported data and on the right side is another View for watching the values and statistics as they are calculated and displayed.
The Current Code
The View that displays the ListView where data is imported to is the ImportProcessView.xaml and it sets its DataContext
to the ImportProcessViewModel.cs
. The VM I've just introduced has a property ObservableCollection<IrData>
that the ListView, I've also just described, binds to. Now to the interesting part...
The ImportProcessView
has a ContentControl
that sets it's content dynamically a UserControl representing the controls and fields specific to the type of Phase that is chosen by the end-user.
<StackPanel Background="White" Margin="5">
<ContentControl Content="{Binding CurrentPhaseView}"/>
</StackPanel>
There are three PhaseViews
, each in its own User Control and each sets it's DataContext
to the ImportProcessViewModel
. As a result I am getting some severe VM bloat to the tune of 2000 lines. Ridiculous. I know. The reason for the bloat is because the ImporProcessViewModel
is maintaining state through properties for each of the three PhaseViews and not only that but contains methods for performing calculations whose data is stored and displayed in these "PhaseViews".
What I am trying to achieve
Obviously before the ImportProcessViewModel
becomes more unwieldy, I need to break it up so that each PhaseView has its own ViewModel, but also such that each ViewModel maintains a relationship back to the ImportProcessViewModel for sake of the dependency imposed by the ObservableCollection of IrData
.
R&D
I've done my research on ViewModels communicating with each other, but most of the results involve applications that were written with a specific MVVM framework. I am not using a framework, and at this point in the project it would be too late to refactor it to start using one.
I did, however, find this article and the answer offered by 'hbarck' suggests something simple like composition to achieve the result I want, but since I don't have much experience with DataTemplates I don't understand what is meant when he/she suggests exposing "the UserControl's ViewModel as a property on the main ViewModel, and bind a ContentControl to this property, which would then instantiate the View (i.e. the UserControl) through a DataTemplate"
Specifically, I don't understand what is meant by "bind a ContentControl to this property which would then instantiate the View through a DataTemplate".
Can someone clarify by way of an code example what is meant by instantiating a view through a DataTemplate in the context of this example?
Additionally, is this a good approach (as suggested by 'hbarck')?
As one can see, I am already setting the Content property of a ContentControl to the Phase View that is to be instantiated. I just don't know know what involving a DataTemplate would look like.
While Benjamin's answer is really elaborate and very helpful, I'd like to clarify how what I wrote in the other post would apply to your problem:
like this:
<DataTemplate DataType="{x:Type my:Phase1VM}">
<my:Phase1View/>
</DataTemplate>
<DataTemplate DataType="{x:Type my:Phase2VM}">
<my:Phase2View/>
</DataTemplate>
<DataTemplate DataType="{x:Type my:Phase3VM}">
<my:Phase3View/>
</DataTemplate>
Notice that there is no x:Key, only the DataType value. If declared like this, WPF would choose the appropriate DataTemplate when asked to display an object of type Phase1VM, Phase2VM or Phase3VM, respectively. Phase1View, Phase2View and Phase3View would be UserControls which would know how to display the different ViewModels. They wouldn't instantiate their ViewModels themselves, but expect that their DataContext is set to an instance of their respective ViewModel from outside.
Under the assumption that the ContentControl which should show the phase view is declared in the main view, and that the DataContext there would be the main ViewModel, you'd declare the ContentControl like this:
<ContentControl Content="{Binding CurrentPhaseVM}"/>
Depending on the actual type of CurrentPhaseVM, this will choose one of the three DataTemplates, and display the appropriate UserControl. The UserControl's DataContext would automatically be the ContentControl's Content, since that would the object which caused the DataTemplate to be chosen.
EDIT: Lists and code formatting don't go together, it seems...
I don't understand what is meant when he/she suggests exposing "the UserControl's ViewModel as a property on the main ViewModel, and bind a ContentControl to this property, which would then instantiate the View (i.e. the UserControl) through a DataTemplate"
A DataTemplate
allows you to specify a relationship between a view (such as a user control) and a view model.
<DataTemplate DataType="{x:Type myApp:MyViewModel}">
<myApp:MyUserControl />
</DataTemplate>
This tells a ContentPresenter
to display MyUserControl
whenever its content property is set to an instance of MyViewModel
. The view model will be used as the user controls DataContext
. Typically, the DataTemplate
is added to your application resources.
What the author of that answer is saying is that you could have a viewModel that has a property of another viewModel type which is bound to the Content
property of the ContentPresenter
.
<ContentPresenter Content="{Binding ParentViewModel.ChildViewModelProperty}"/>
Providing you have a DataTemplate
that specifies a relationship between your ChildViewModel
and your user control, WPF will automatically load the user control into your view.
This answer I provided to another question might also provide you with some help.
I need to break it up so that each PhaseView has its own ViewModel, but also such that each ViewModel maintains a relationship back to the ImportProcessViewModel.
This will allow you to break your viewModels into smaller, more manageable viewModels that look after themselves. This will leave you with the problem of communicating between the viewModels.
If you nest your viewModels as suggested, then your child viewModels could expose events that the parent viewModel can bind to so it is notified when something changes. Something like this:
public class ParentViewModel // Derive from some viewModel base that implements INPC
{
public ParentViewModel()
{
childViewModel = new ChildViewModel();
childViewModel.SomeEvent += someEventHandler;
// Don't forget to un-subscribe from the event at some point...
}
private void SomeEventHandler(object sender, MyArgs args)
{
// Update your calculations from here...
}
}
This is simple and doesn't require any additional frameworks. Some might argue against this method but it is a valid solution that works. The downside is that the viewModels have to know about each others existence in order to subscribe to the events so can end up being tightly-coupled. You can use standard object-oriented design principles to get around this though (I.E. derive your child viewModel from an interface so that the parent only knows about the interface and not the implementation).
If you really want to go for loosely-coupled communication then you need to use some sort of event aggregation or message bus system. This is similar to the above method except there is an object that sits between the view models and acts as a mediator so that the viewModels do not have to know of each others existence. My answer here provides some more information.
There are pre-existing solutions available but this would involve taking on an additional framework. I would advise using Josh Smiths MVVM foundation as it is very simple and you would only need to use a single class anyway.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With