I'm new to WPF/MVVM and building an application which runs "Jobs". The aplication currently looks like this:
The MainView
contains some buttons and a ContentControl
. The ContentControl
shows some sub-views like JobsView
or ApplicationSettingsView
, depending on the selected menu item.
The JobsView
contains blocks which represents a job. It shows some job information, job functions and an edit button (Bewerk) to edit/configure the job.
When i click on the button "Bewerk", a job specific edit view should be shown in the ContentControl
of the MainView
. I'm looking for the best way to handle this navigation. Important is that i have the instance of the concerning job available in the JobEditViewModel
.
I tried:
JobsViewModel
(https://linuxhint.com/passing-function-as-parameter-c-sharp/) when its created in the MainViewModel
. This does not work as the JobsViewModel
is called twice somehow (noticed during debugging). One time from the MainViewModel
and one time from ???. The second time, it calls the constructor without params so i cannot call the passed function. After some research i also read that its bad practice to pass params to ViewModel
constructors....
namespace ECRO.MVVM.ViewModel
{
internal class MainViewModel : ObservableObject
{
private object _currentView;
private Engine engine;
public JobsViewModel JobsVm { get; set; }
public SettingsViewModel SettingsVm { get; set; }
public RelayCommand JobViewCommand { get; set; }
public RelayCommand SettingsViewCommand { get; set; }
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
Task.Run(() =>
{
engine = Engine.Instance;
});
JobsVm = new JobsViewModel((viewModel) => { this._currentView = viewModel; });
SettingsVm = new SettingsViewModel();
_currentView = JobsVm;
// Initialize relay commands
JobViewCommand = new RelayCommand(o => { CurrentView = JobsVm; });
SettingsViewCommand = new RelayCommand(o => { CurrentView = SettingsVm; });
}
}
}
...
namespace ECRO.MVVM.ViewModel
{
class JobsViewModel
{
private ObservableCollection<RJob> _jobs;
public ObservableCollection<RJob> Jobs { get => _jobs; set { _jobs = value; } }
public RelayCommand ForceRunJobOnceCmd { get; set; }
public RelayCommand StartJobCmd { get; set; }
public RelayCommand StopJobCmd { get; set; }
public RelayCommand EnableJobCmd { get; set; }
public RelayCommand DisableJobCmd { get; set; }
public RelayCommand EditJobCmd { get; set; }
public string btnEnableDisableContent { get => "test"; }
public JobsViewModel() {
}
public JobsViewModel(Action<BaseViewModel> changeContentView)
{
_jobs = Engine.Instance.RJobs;
ForceRunJobOnceCmd = new RelayCommand(o => { ((RJob)o).ForceRunOnce(); });
StartJobCmd = new RelayCommand(o => { ((RJob)o).Start(); });
StopJobCmd = new RelayCommand(o => { ((RJob)o).Stop(); });
EnableJobCmd = new RelayCommand(o => { ((RJob)o).Enable(); });
DisableJobCmd = new RelayCommand(o => { ((RJob)o).Disable(); });
EditJobCmd = new RelayCommand(o => { changeContentView(new VerzamelqueryViewModel()); });
}
}
}
MainView
is not called/triggered for some reason. It also feels not nice to go for this solution as i have no idea how i can register an event handler when the sub-view is not created/initiated from the MainView
but from another sub-view. (e.g. the EditJobView
will get a sub-sub view to show logs of the concerning Job
and include a back button to go back to the EditJobView
) Its also feels not realy generic/reusable. (need to implement a new event and event handler for each sub-view that handles navigation)...
namespace ECRO.MVVM.ViewModel
{
internal class MainViewModel : ObservableObject
{
private object _currentView;
private Engine engine;
public JobsViewModel JobsVm { get; set; }
public SettingsViewModel SettingsVm { get; set; }
public RelayCommand JobViewCommand { get; set; }
public RelayCommand SettingsViewCommand { get; set; }
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
Task.Run(() =>
{
engine = Engine.Instance;
});
//JobsVm = new JobsViewModel((viewModel) => { });
JobsVm = new JobsViewModel();
JobsVm.ChangeView += (object sender, BaseViewModel newView) => { _currentView = newView; };
SettingsVm = new SettingsViewModel();
_currentView = JobsVm;
// Initialize relay commands
JobViewCommand = new RelayCommand(o => { CurrentView = JobsVm; });
SettingsViewCommand = new RelayCommand(o => { CurrentView = SettingsVm; });
}
}
}
...
namespace ECRO.MVVM.ViewModel
{
class JobsViewModel
{
private ObservableCollection<RJob> _jobs;
public ObservableCollection<RJob> Jobs { get => _jobs; set { _jobs = value; } }
public RelayCommand ForceRunJobOnceCmd { get; set; }
public RelayCommand StartJobCmd { get; set; }
public RelayCommand StopJobCmd { get; set; }
public RelayCommand EnableJobCmd { get; set; }
public RelayCommand DisableJobCmd { get; set; }
public RelayCommand EditJobCmd { get; set; }
public string btnEnableDisableContent { get => "test"; }
public event EventHandler<BaseViewModel> ChangeView;
public JobsViewModel()
{
_jobs = Engine.Instance.RJobs;
ForceRunJobOnceCmd = new RelayCommand(o => { ((RJob)o).ForceRunOnce(); });
StartJobCmd = new RelayCommand(o => { ((RJob)o).Start(); });
StopJobCmd = new RelayCommand(o => { ((RJob)o).Stop(); });
EnableJobCmd = new RelayCommand(o => { ((RJob)o).Enable(); });
DisableJobCmd = new RelayCommand(o => { ((RJob)o).Disable(); });
EditJobCmd = new RelayCommand(o => { ChangeView?.Invoke(this, new VerzamelqueryViewModel()); });
}
}
}
Whats the best practice/WPF-MVVM way to handle my case?
------- Udate: Views in case they are needed ...
MainView:
<Window x:Class="ECRO.MVVM.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ECRO.MVVM.View"
xmlns:viewModel="clr-namespace:ECRO.MVVM.ViewModel"
mc:Ignorable="d"
Height="880" Width="1000"
WindowStyle="None"
ResizeMode="NoResize"
Background="Transparent"
AllowsTransparency="True"
Title="MainWindow">
<Window.DataContext>
<viewModel:MainViewModel/>
</Window.DataContext>
<Border
Background="#272537"
CornerRadius="20">
<WrapPanel>
<Border Height="35"
Width="1000"
Margin="0"
CornerRadius="20 20 0 0"
Background="#22202f">
<DockPanel>
<DockPanel Width="930"
Background="Transparent"
PreviewMouseDown="drawWindow">
<TextBlock Text="ECRO - ECB Robot" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="15" FontWeight="Bold"/>
</DockPanel>
<DockPanel HorizontalAlignment="Right"
Margin="0 5 10 0">
<Button Content="—"
Width="20" Height="20"
Margin="5 5 5 5"
Background="Transparent"
BorderThickness="0"
Foreground="White"
FontSize="15"
Click="minimizeApplication"/>
<Button Content="X"
Width="20" Height="20"
Margin="5 5 5 5"
Background="Transparent"
BorderThickness="0"
Foreground="White"
FontSize="15"/>
</DockPanel>
</DockPanel>
</Border>
<Grid Height="845" Width="1000">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="ECB Robot"
Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Foreground="White"
FontSize="22"
Margin="20 0 0 0"/>
<StackPanel Grid.Row="1"
Margin="0 10 0 0">
<RadioButton Content="Jobs"
Style="{StaticResource MenuButton}"
IsChecked="True"
Command="{Binding JobViewCommand}"
/>
<RadioButton Content="Settings"
Style="{StaticResource MenuButton}"
Command="{Binding SettingsViewCommand}"/>
<RadioButton Content="Stop robot"
Style="{StaticResource MenuButton}"/>
</StackPanel>
<TextBox Width="250"
Height="40"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
Margin="5"
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource Textbox}"/>
<ContentControl Grid.Row="1"
Grid.Column="1"
Margin="10"
Content="{Binding CurrentView}"/>
</Grid>
</WrapPanel>
</Border>
</Window>
JobsView:
<UserControl x:Class="ECRO.MVVM.View.JobsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:ECRO.MVVM.ViewModel"
xmlns:widget="clr-namespace:ECRO.MVVM.Widgets"
mc:Ignorable="d" d:DesignWidth="800" Height="780">
<UserControl.DataContext>
<viewModel:JobsViewModel/>
</UserControl.DataContext>
<StackPanel>
<TextBlock Text="Jobs" Style="{StaticResource HeaderTextBlock}"/>
<ScrollViewer Width="780" Height="700">
<ItemsControl ItemsSource="{Binding Path=Jobs}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type viewModel:JobWidgetViewModel}">
<widget:JobWidget/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</UserControl>
JobWidget:
<UserControl x:Class="ECRO.MVVM.Widgets.JobWidget"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="110" d:DesignWidth="760">
<!--<UserControl.DataContext>
<viewModel:JobWidgetViewModel/>
</UserControl.DataContext>-->
<Grid>
<Grid Height="110" Width="760">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="677*"/>
<ColumnDefinition Width="123*"/>
</Grid.ColumnDefinitions>
<Border CornerRadius="10"
Background="#353340"
Height="100"
Margin="10,0,10,0"
VerticalAlignment="Center" Grid.ColumnSpan="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50*"/>
<!--<RowDefinition Height="50*"/>-->
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical"
Margin="5 2 0 0">
<Label Content="Naam" FontWeight="Bold" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="Status" FontWeight="Bold" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="Laatste run" FontWeight="Bold" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="Volgende run" FontWeight="Bold" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="Schema" FontWeight="Bold" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
</StackPanel>
<StackPanel Grid.Column="1" Margin="2">
<Label Content="{Binding Name}" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="{Binding Status}" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="{Binding LastRun}" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="{Binding NextRun}" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
<Label Content="{Binding Schedule.Type }" Style="{StaticResource DefaultLabel}" Margin="0 -5 0 -5"/>
</StackPanel>
<Border Grid.RowSpan="2"
Grid.Column="2"
Grid.Row="0"
HorizontalAlignment="Right"
Margin="0 0 20 0">
<StackPanel Orientation="Horizontal">
<Button Content="Forceer run"
Style="{StaticResource DarkButton}"
Width="100"
Command="{Binding DataContext.ForceRunJobOnceCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button Content="Start"
Style="{StaticResource DarkButton}"
Command="{Binding DataContext.StartJobCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button Content="Stop"
Style="{StaticResource DarkButton}"
Command="{Binding DataContext.StopJobCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button x:Name="btnEnable"
Content="Enable"
Style="{StaticResource DarkButton}"
Command="{Binding DataContext.EnableJobCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button x:Name="btnDisable"
Content="Disable"
Style="{StaticResource DarkButton}"
Command="{Binding DataContext.DisableJobCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
<Button Content="Bewerk"
Style="{StaticResource DarkButton}"
Command="{Binding Main.EditJobCmd, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</UserControl>
You could use an event aggregator or a messenger to raise events or pass messages from one view model to another. The concept is explained in this blog post.
The idea is that the JobsViewModel
raises a "navigation" event that the MainViewModel
handles without introducing a direct dependency between the view models.
Another option is to inject JobsViewModel
with an INavigate
or INavigationService
interface that is implemented to switch the view, for example by doing what the MainViewModel
currently does.
A third option is to bind directly to a command of the MainViewModel
from the JobsView
using a {RelativeSource}
:
<Button Content="..." Command="{Binding Path=DataContext.SwitchViewCommand,
RelativeSource={RelativeSource AncestorType=Window}}" />