How to access one ViewModel propery/ Data in another ViewModel - c#

Ya I googled it many times, and searched in many forums, but I did not got what exactly what i want.
In simple terms, I have Main window, in that first I should display LoginUserControl, when user click on button present in LoginUserControl, it should move to GameUserControl, again when he clicks on button in GameUserControl, it should move to LoginUserControl.
I'm using MVVM pattern and Unity for DependencyInjection (when i searched, I heard its easy to maintain the instances through it).
Here is what I tried till now :
MainView:
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding ShowControl}" />
</Grid>
MainViewModel :
class MainWindowViewModel : Bindable, IMainViewModel
{
private UserControl showControl;
public UserControl ShowControl
{
get
{
if (showControl == null)
showControl = App.Container.Resolve<LoginView>();
return showControl;
}
set
{
SetProperty(ref showControl, value);
}
}
}
LoginView :
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="0.5*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="{Binding Display}" />
<Button Grid.Row="1" Content="Navigate to Game View" Command="{Binding Navigae}" />
<TextBox Text="{Binding MyData, Mode=TwoWay}" Grid.Row="2" Margin="52,24,76,31" />
</Grid>
LoginView.cs :
public LoginView(IMainViewModel mainViewModel)
{
this.DataContext = new LoginViewModel(mainViewModel);
InitializeComponent();
}
LoginViewModel :
public class LoginViewModel : Bindable
{
// Bindable which implements INotifyPropertyChanged Event
IMainViewModel mainViewModel;
public LoginViewModel(IMainViewModel mainViewModel)
{
this.mainViewModel = mainViewModel;
this.Navigae = new RelayCommand(execute, canExecute);
display = "login view";
myData = "Gopi ";
}
private bool canExecute(object arg)
{
return true;
}
private void execute(object obj)
{
// mainViewModel.ShowControl = App.Container.Resolve<GameView>();
// here I want to access the ShowControl property from MainViewModel and update it
}
public ICommand Navigae
{
get; set;
}
private string display;
public string Display
{
get
{
return display;
}
set
{
SetProperty(ref display, value);
}
}
private string myData;
public string MyData
{
get
{
return myData;
}
set
{
myData = value;
}
}
}
In the same way I have GameView, GameView.cs and GameViewModel (only the name changes w.r.t. LoginView)
IMainViewModel : (It is used as Repository which I seen in some examples)
public interface IMainViewModel
{
}
I'm using unity because I dont want to create new Instances every time when I clicked the button. when we are using unity it will create instance for first time and use the same for later.
Here is the code of App.cs :
public partial class App : Application
{
private static IUnityContainer _container;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ConfigureContainer();
Application.Current.MainWindow = _container.Resolve<MainWindow>();
Application.Current.MainWindow.Show();
}
private void ConfigureContainer()
{
_container = new UnityContainer();
_container.RegisterType<IMainViewModel, MainWindowViewModel>(new ContainerControlledLifetimeManager());
}
public static IUnityContainer Container
{
get
{
return _container;
}
}
}
Can anyone guide me from this point.
Thanks in advance :)

I suggest you consider viewmodel first navigation.
As an aside / observation:
Resolving MainWindowViewModel out of unity seems to be because you want to pass it into the other viewmodels. And that's all you're passing into the child viewmodels. Even if you intend mocking mainwindowviewmodel then you could just do so by instantiating your mock in your test code.
Your dependency injection just seems to be complicating things at this stage.
/ Aside.
Consider the approach in this:
https://social.technet.microsoft.com/wiki/contents/articles/52485.wpf-tips-do-not-use-frame-and-page-for-navigation.aspx
MainWindowViewmodel controls navigation.
You want to do that from buttons in child controls in code like the sample? No problem. You can use relativesource binding on your command and bind a button in the child control to a command in mainwindowviewmodel. You could Bind the commandparameter from the child viewmodel if you need some data from that.
That mainwindowviewmodel then instantiates any child viewmodels rather than their usercontrol doing so. Mainwindowviewmodel could pass a reference to itself in if that was still necessary.

Related

How do I go to another view from a view in MVVM WPF?

Here I have a WPF application that is made with the MVVM structure. I am fairly new to C# WPF and am not familiar with this concept. I am attempting to switch to another view through a function in one view via the press of a button.
Here is what the application looks like,
Once the Login button is pressed a function is triggered that will validate the inputs and if valid switch to another view. Which would look like such,
File Structure
How can i switch the views ?
Below are some code for reference.
MainWindow.xaml
<Window x:Class="QuizAppV2.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:QuizAppV2"
xmlns:viewModel="clr-namespace:QuizAppV2.MVVM.ViewModel"
mc:Ignorable="d"
Height="600" Width="920"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="NoResize"
Background="Transparent"
AllowsTransparency="True">
<Window.DataContext>
<viewModel:MainViewModel/>
</Window.DataContext>
<Border Background="#272537"
CornerRadius="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="75"/>
<RowDefinition/>
<RowDefinition Height="25"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Online Quiz"
Grid.Column="1"
FontSize="20"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="2"
Margin="30,20"
Orientation="Horizontal"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<Button Content="–"
Background="#00CA4E"
Style="{StaticResource UserControls}"
Click="Minimise"/>
<Button Content="▢"
Background="#FFBD44"
Style="{StaticResource UserControls}"
Click="Restore"/>
<Button Content="X"
Background="#FF605C"
Style="{StaticResource UserControls}"
Click="Exit"/>
</StackPanel>
</Grid>
<ContentControl Grid.Column="1"
Grid.Row="1"
Margin="20,10,20,50"
Content="{Binding CurrentView}"/>
</Grid>
</Border>
</Window>
MainViewModel.cs
using QuizAppV2.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QuizAppV2.MVVM.ViewModel
{
class MainViewModel : ObservableObject
{
public RelayCommand LoginViewCommand { get; set; }
public RelayCommand SubjectSelectionViewCommand { get; set; }
public RelayCommand QuizViewCommand { get; set; }
public RelayCommand ResultViewCommand { get; set; }
public LoginViewModel LoginVM { get; set; }
public SubjectSelectionViewModel SubjectSelectVM { get; set; }
public QuizViewModel QuizVM { get; set; }
public ResultViewModel ResultVM { get; set; }
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
onPropertyChanged();
}
}
public MainViewModel()
{
LoginVM = new LoginViewModel();
SubjectSelectVM = new SubjectSelectionViewModel();
QuizVM = new QuizViewModel();
ResultVM = new ResultViewModel();
CurrentView = SubjectSelectVM;
LoginViewCommand = new RelayCommand(o =>
{
CurrentView = LoginVM;
});
SubjectSelectionViewCommand = new RelayCommand(o =>
{
CurrentView = SubjectSelectVM;
});
}
}
}
LoginView.xaml
using QuizAppV2.MVVM.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace QuizAppV2.MVVM.View
{
/// <summary>
/// Interaction logic for LoginView.xaml
/// </summary>
public partial class LoginView : UserControl
{
public LoginView()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (UsrId.Text == "" || UsrName.Text == "")
{
UsrIDErrMsg.Visibility = Visibility.Visible;
UsrNameErrMsg.Visibility = Visibility.Visible;
}
else
{
UsrIDErrMsg.Visibility = Visibility.Hidden;
UsrNameErrMsg.Visibility = Visibility.Hidden;
MainWindow.currentUser = new Student(UsrId.Text, UsrName.Text);
}
}
}
}
Thank you
I suggest using "Datatemplate". Put in the main window resources the following:
<DataTemplate DataType="{x:Type viewmodel:QuizViewModel}">
<local:QuizView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodel:LoginViewModel}">
<local:LoginView/>
</DataTemplate>
and so on with the others...
WPF is doing all the work for you, it examine the "CurrentView" prroperty and select how to view it according the the suitable DataTemplate.
Navigation is a tricky topic there are few ways to do this but since you are new to WPF I tried to outline a simple technique, along the lines of the examples you've provided requirement is have to go from page to page, a simple idea would be to swap out the contents. What I mean by that is when the user clicks "Login" we authenticate the user and swap the LoginPage with some other page, in your case a quiz page, when the user selection any option we swap out the view with the next view and so on.
I've coded up a simple solution with Shell mechanism. Essentially we create a empty shell in MainWindow (i.e it has no UI) and we load pages into this empty shell using a NavigationService/Helper. I've gone with a singleton class here just for simplicity, there are 3 main Methods in this,
RegisterShell : This has to be the Window where the swapping will happen, this ideally needs to be set once.
Load View : Method which Swaps out old view with the new one, I have gone with user control for this as most of the sub views can be user control in WPF.
LoadViewWithCustomData : Similar to above but has more flexibilty since it allows you to supply extra data.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace Navigation
{
class NavigationService
{
/// <summary>
/// Singleton so we keep on shell which views can use this to navigate to different pages.
/// </summary>
public static NavigationService Instance = new NavigationService();
private MainWindow myShell;
private NavigationService()
{
}
/// <summary>
/// Register the main shell so this service know where to swap the data out and in of
/// </summary>
/// <param name="theShell"></param>
public void RegisterShell(MainWindow theShell)
{
this.myShell = theShell;
}
/// <summary>
/// Swaps out any view to the shell.
/// </summary>
/// <typeparam name="T"></typeparam>
public void LoadView<T>() where T : UserControl, new()
{
myShell.TheShell = new T();
}
/// <summary>
/// Swaps out any view to the shell with custom data, here the user responsible to create UserControl with all the reqired data for the view.
/// We can automate this via reflection if required.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="theNewControl"></param>
public void LoadViewWithCustomData<T>(UserControl theNewControl) where T : UserControl, new()
{
myShell.TheShell = theNewControl;
}
}
Now here's how my LoginPage looks, the important line here is NavigationService.Instance.LoadView<_4OptionQuizPage>() this essentially sends the user to _4OptionQuizPage.
public partial class LoginPage : UserControl
{
public ICommand LoginClicked { get; }
public LoginPage()
{
InitializeComponent();
this.DataContext = this;
LoginClicked = new SimpleCommand(OnLoginClicked);
}
private void OnLoginClicked()
{
// TODO : Authenticate user here.
// Send the user to Quiz Page
NavigationService.Instance.LoadView<_4OptionQuizPage>();
}
}
And in the _4OptionQuizPage we can have something like this, this is where the bulk of business logic may reside, I have 4 buttons here, 2 of them show message box but Button 1 sends you back to LoginPage and Button 2 reloads the same page with different data (i.e sending the user to next question)
public partial class _4OptionQuizPage : UserControl, INotifyPropertyChanged
{
public ICommand Option1Clicked { get; }
public ICommand Option2Clicked { get; }
public ICommand Option3Clicked { get; }
public ICommand Option4Clicked { get; }
private string myQuestion;
public event PropertyChangedEventHandler PropertyChanged;
public string Question
{
get { return myQuestion; }
set
{
myQuestion = value;
NotifyPropertyChanged();
}
}
public _4OptionQuizPage() : this($"Question Loaded At {DateTime.Now}, this can be anything.")
{
}
public _4OptionQuizPage(string theCustomData)
{
InitializeComponent();
Question = theCustomData;
this.DataContext = this;
this.Option1Clicked = new SimpleCommand(OnOption1Clicked);
this.Option2Clicked = new SimpleCommand(OnOption2Clicked);
this.Option3Clicked = new SimpleCommand(OnOption3Clicked);
this.Option4Clicked = new SimpleCommand(OnOption4Clicked);
}
private void OnOption4Clicked()
{
MessageBox.Show("Option 4 selected, Store the results");
}
private void OnOption3Clicked()
{
MessageBox.Show("Option 3 selected, Store the results");
}
private void OnOption1Clicked()
{
NavigationService.Instance.LoadView<LoginPage>();
}
private void OnOption2Clicked()
{
NavigationService.Instance.LoadViewWithCustomData<LoginPage>(new _4OptionQuizPage("A custom question to emulate custom data"));
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Finally your MainWindow would be registering the shell and sending the user to LoginPage, and it's XAML file should not have anything in it
public partial class MainWindow : Window, INotifyPropertyChanged
{
private object myShell;
public object TheShell
{
get { return myShell; }
set
{
myShell = value;
this.NotifyPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
NavigationService.Instance.RegisterShell(this);
NavigationService.Instance.LoadView<LoginPage>();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml should be empty, essentially a shell for everything else.
<Window x:Class="Navigation.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:Navigation"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Content="{Binding TheShell}">
</Window>
This sample demonstrates two approaches to navigation. Often useful since you say want to start by logging in but not show any menus etc until the user is logged in. Then once they log in you want some sort of menu or list of views they can navigate to which remains static.
My mainwindow is purely a shell to contain everything.
It's markup is:
<Window ......
Title="{Binding Title}"
Content="{Binding}"
/>
This sample uses viewmodel first for all navigation. Viewmodels are templated out into UI.
There is more in the code behind.
public partial class LoginNavigationWindow : Window
{
public Type ParentViewModel
{
get { return (Type)GetValue(ParentViewModelProperty); }
set { SetValue(ParentViewModelProperty, value); }
}
public static readonly DependencyProperty ParentViewModelProperty =
DependencyProperty.Register(name: "ParentViewModel",
propertyType: typeof(Type),
ownerType: typeof(LoginNavigationWindow),
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: null,
propertyChangedCallback: new PropertyChangedCallback(ParentViewModelChanged)
));
private static void ParentViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var vm = Activator.CreateInstance((Type)e.NewValue);
((Window)d).DataContext = vm;
Task.Run(((IInitiatedViewModel)vm).Initiate);
}
public LoginNavigationWindow()
{
InitializeComponent();
WeakReferenceMessenger.Default.Register<ParentNavigate>(this, (r, pn) =>
{
this.SetValue(LoginNavigationWindow.ParentViewModelProperty, pn.ParentViewModelType);
});
}
The messenger registration will switch out the window's datacontext using a dependency property. The message is just a class with a property to pass a Type
public class ParentNavigate
{
public Type ParentViewModelType { get; set; }
}
The callback ParentViewModelChanged takes a type, instantiates it and sets datacontext on the window.
Usually, you're not interested in retaining state of a window or parent level piece of view. You already logged in. If you wanted to log back in again then you would start again and input name and password.
The entrypoint is slightly unusual since I handle application startup and rely on that dependency property callback.
private void Application_Startup(object sender, StartupEventArgs e)
{
var mw = new LoginNavigationWindow();
mw.Show();
mw.SetValue(LoginNavigationWindow.ParentViewModelProperty, typeof(LoginViewModel));
}
Instead of a mainwindow full of menus etc I have of course got nothing.
I have a LoginUC is the first thing you will see on start up. This is just illustrative.
We will get input from the user and validate it before navigating in a real app. We're just interested in that navigation here so this version just has a button to navigate to MainViewModel:
<Grid>
<StackPanel>
<TextBlock Text="Log in"/>
<Button Content="Go"
Command="{Binding LoadMainCommand}"/>
</StackPanel>
</Grid>
</UserControl>
My LoginViewModel has a command, title and a task.
public partial class LoginViewModel : BaseParentViewModel
{
[RelayCommand]
private async Task LoadMain()
{
var pn = new ParentNavigate{ ParentViewModelType = typeof(MainViewModel) };
WeakReferenceMessenger.Default.Send(pn);
}
public LoginViewModel()
{
Title = "Please Log In first";
}
public override async Task Initiate()
{
// Get any data for login here
}
}
BaseParentViewModel
public partial class BaseParentViewModel : ObservableObject, IInitiatedViewModel
{
[ObservableProperty]
private string title = string.Empty;
virtual public async Task Initiate() { }
}
Interface
public interface IInitiatedViewModel
{
Task Initiate();
}
The purpose of this interface is to give us a generic way for any viewmodel to get any data it requires. By setting datacontext and then starting up a background thread to get that data the view will appear quickly and then fill with any data it needs. If getting that data takes a while then at least the view is "up" and visible quickly whilst the task still carries on working.
In a fuller example we would have IsBusy in a base viewmodel which would start off true and be changed to false. That would drive a "throbber" or busing indicator in the view.
A resource dictionary associates viewmodel datatemplates with usercontrols using datatype:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LoginNavigation"
>
<DataTemplate DataType="{x:Type local:MainViewModel}">
<local:MainUC/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:LoginViewModel}">
<local:LoginUC/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SubjectsViewModel}">
<local:SubjectsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ResultViewModel}">
<local:ResultView/>
</DataTemplate>
</ResourceDictionary>
That is merged in app.xaml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/Resources/ViewDataTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Once you login, the entire content of the window is replaced. The datacontext is changed from LoginViewModel to MainViewModel, that is then templated out into MainUC:
public partial class MainViewModel : BaseParentViewModel
{
[ObservableProperty]
private object currentChildViewModel;
[ObservableProperty]
private List<ChildViewModel> childViewModelList;
[RelayCommand]
private async Task ChildNavigation(ChildViewModel cvm)
{
if (cvm.Instance == null)
{
cvm.Instance = Activator.CreateInstance(cvm.ViewModelType);
if (cvm.Instance is IInitiatedViewModel)
{
Task.Run(((IInitiatedViewModel)cvm.Instance).Initiate);
}
}
CurrentChildViewModel = cvm.Instance;
}
public override async Task Initiate()
{
ChildViewModelList = new List<ChildViewModel>()
{
new ChildViewModel{ DisplayName="Subjects", ViewModelType= typeof(SubjectsViewModel) },
new ChildViewModel{ DisplayName="Results", ViewModelType= typeof(ResultViewModel) }
};
}
public MainViewModel()
{
Title = "Quiz";
}
}
You would probably want to have more views of course and pick one to show initially which would be setup in Initiate.
MainUC:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding ChildViewModelList}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding DisplayName}"
Command="{Binding DataContext.ChildNavigationCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentPresenter Content="{Binding CurrentChildViewModel}"
Grid.Column="1"/>
</Grid>
</UserControl>
In the view you get a list of buttons in a left column which will allow navigation in the right column. But retaining MainUC of course.
Instead of a listbox this could be a menu or maybe a tabcontrol.
Clicking on a button calls a command in MainViewModel and passes the instance of ChildViewModel as a parameter.
That is then used to instantiate a viewmodel, set CurrentChildViewmodel and cache the instance.
CurrentChildViewmodel will of course itself be templated out into a usercontrol within MainUC.
public partial class ChildViewModel : ObservableObject
{
public string DisplayName { get; set; }
public Type ViewModelType { get; set; }
public object Instance { get; set; }
}
This is rather a simplistic approach and in a real world substantial app you would want dependency injection, factories and the like. But this is already quite a bit of code for a Stack Overflow answer as it is.
The remaining viewmodels and views are just simplistic implementations to prove it all works. eg
public partial class SubjectsViewModel : ObservableObject, IInitiatedViewModel
{
public async Task Initiate()
{
// Get any data for Subjects here
}
}
and
<Grid>
<TextBlock Text="Subjects"/>
</Grid>
</UserControl>
There are many ways how to allow a view model to participate in page navigation.
In general, each class that participates in navigation has to have access to your navigation API.
For example, you could move the navigation logic to a dedicated class NavigationService and provide a shared reference to every class that should be able to navigate to a different view.
Alternatively (and recommended), you can use routed commands that you handle on the MainWindow, which then delegates the command to the MainViewModel.
In this scenario each button would have to pass the destination as CommandParameter. This solution allows the particular view models to not directly participate in the navigation. You don't need to pollute your view model classes with navigation details.
The following example shows how to navigate from the QuizView to the ResultView using a RoutedCommand.
MainViewModel.cs
The MainViewModel is the only view model class that knows how to navigate and about the related details.
This enables extensibility while keeping the implementation of the view model classes simple.
In general, to enable data validation let the view models implement INotifyDataErrorInfo.
You can then query the INotifyDataErrorInfo.HasErrors property before allowing to leave a page.
class MainViewModel : ObservableObject
{
public object CurrentView { get; set; }
private Dictionary<Type, INotifyPropertyChanged> ViewModelMap { get; }
public MainViewModel()
{
this.ViewModelMap = new Dictionary<Type, INotifyPropertyChanged>
{
{ typeof(QuizVm), new QuizVm() },
{ typeof(ResultVm), new ResultVm() },
};
}
// Check if destination type is valid.
// In case the navigation source implements INotifyDataErrorInfo,
// check if the source is in a valid state (INotifyDataErrorInfo.HasEWrrors returns 'false').
// This method is called by the view. It will delegate its ICommand.CanExecute to this method
// If this method returns 'false' the command source e.g. Button will be disabled.
public bool CanNavigate(Type navigationSourceType, Type navigationDestinationType)
=> CanNavigateAwayFrom(navigationSourceType)
&& CanNavigateTo(navigationDestinationType);
private bool CanNavigateAwayFrom(Type navigationSourceType)
=> this.ViewModelMap.TryGetValue(navigationSourceType, out INotifyPropertyChanged viewModel)
&& viewModel is INotifyDataErrorInfo notifyDataErrorInfo
? !notifyDataErrorInfo.HasErrors
: true;
private bool CanNavigateTo(Type navigationDestinationType)
=> this.ViewModelMap.ContainsKey(navigationDestinationType);
// This method is called by the view. It will delegate its ICommand.Execute to this method
public void NavigateTo(Type destinationType)
{
if (this.ViewModelMap.TryGetValue(destinationType, out INotifyPropertyChanged viewModel))
{
this.CurrentView = viewModel;
}
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
public static RoutedCommand NavigateCommand { get; } = new RoutedUICommand(
"Navigate to view command",
nameof(NavigateCommand),
typeof(MainWindow));
private MainViewModel MainViewModel { get; }
public MainWindow()
{
InitializeComponent();
this.MainViewModel = new MainViewModel();
this.DataContext = this.MainViewModel;
var navigateCommandBinding = new CommandBinding(MainWindow.NavigateCommand, ExecuteNavigateCommand, CanExecuteNavigateCommand);
this.CommandBindings.Add(navigateCommandBinding);
}
private void CanExecuteNavigateCommand(object sender, CanExecuteRoutedEventArgs e)
{
if (e.Source is not FrameworkElement commandSource)
{
return;
}
Type navigationSourceType = commandSource.DataContext.GetType();
Type navigationDestinationType = (Type)e.Parameter;
e.CanExecute = this.MainViewModel.CanNavigate(navigationSourceType, navigationDestinationType);
}
private void ExecuteNavigateCommand(object sender, ExecutedRoutedEventArgs e)
{
var destinationViewModelType = (Type)e.Parameter;
this.MainViewModel.NavigateTo(destinationViewModelType);
}
}
MainWindow.xaml
To actually render the views (for example a custom Control) you need to define an implicit DataTemplate (without the x:Key directive) that has the associated view model class as DataType. The ContentControl will then automatically pick the correct one that matches the type of the ContentControl.Content property value.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type local:QuizVM}">
<QuizView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ResultVM}">
<ResultView />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentView}" />
</Window>
If a view needs to navigate, it must use the static routed command (defined and handled in the MainWindow) and pass the Type of the destination view model as CommandParameter.
This way, navigation will not pollute the view models and stays within the view.
QuizView.xaml
<QuizView>
<Button Content="Next"
Command="{x:Static local:MainWindow.NextPageCommand}"
CommandParameter="{x:Type local:ResultVM}"/>
</QuizView>
ResultView.xaml
<ResultView>
<Button Content="Back"
Command="{x:Static local:MainWindow.NextPageCommand}"
CommandParameter="{x:Type local:QuizVM}"/>
</ResultView>
Because the view model classes generally don't directly participate in the navigation,
they don't have to implement any related commands or depend on any navigation service.
Navigation is completely controlled by the MainWindow and its MainViewModel.
For optional data validation let them implement INotifyDataErrorInfo.
QuizVM.cs
class QuizVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}
ResultVM.cs
class ResultVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}

Bind property between sibling user controls

I have the below problem: I have two different user controls inside a parent user control. These are trainList, which holds a list of train objects and trainView, which is an user control that shows details of the selected train in the list.
My wish is to share a variable of trainList with trainView.
What I have now is:
Parent user control:
<UserControl>
<UserControl>
<customControls:trainList x:Name="trainList"></customControls:trainList>
</UserControl>
<UserControl>
<customControls:trainView x:Name="trainView"></customControls:trainView>
</UserControl>
<TextBlock DataContext="{Binding ElementName=trainList, Path=SelectedTrain}" Text="{ Binding SelectedTrain.Id }">Test text</TextBlock>
</UserControl>
TrainList class:
public partial class TrainList : UserControl
{
public TrainList()
{
this.InitializeComponent();
this.DataContext = this;
}
public Train SelectedTrain { get; set; }
public void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.Print(this.SelectedTrain.Id);
}
}
Note: The Train class implements INotifyPropertyChanged.
If I got this to work, I'd apply the binding to the trainView user control (not sure if this would work) instead to the text block.
<UserControl>
<customControls:trainView x:Name="trainView" DataContext="{Binding ElementName=trainList, Path=SelectedTrain}"></customControls:trainView>
</UserControl>
And then, I would access that variable someway from the code-behind of trainView.
(And after this, I would like to share a different variable from trainView with its parent user control, but maybe that's another question).
My current question is: could this be done this way or would I need to follow another strategy?
Take this simple view model, with a base class that implements the INotifyPropertyChanged interface, and a Train, TrainViewModel and MainViewModel class.
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(
ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (!Equals(storage, value))
{
storage = value;
OnPropertyChanged(propertyName);
}
}
}
public class Train : ViewModelBase
{
private string name;
public string Name
{
get { return name; }
set { SetValue(ref name, value); }
}
private string details;
public string Details
{
get { return details; }
set { SetValue(ref details, value); }
}
// more properties
}
public class TrainViewModel : ViewModelBase
{
public ObservableCollection<Train> Trains { get; }
= new ObservableCollection<Train>();
private Train selectedTrain;
public Train SelectedTrain
{
get { return selectedTrain; }
set { SetValue(ref selectedTrain, value); }
}
}
public class MainViewModel
{
public TrainViewModel TrainViewModel { get; } = new TrainViewModel();
}
which may be initialized in the MainWindow's constructor like this:
public MainWindow()
{
InitializeComponent();
var vm = new MainViewModel();
DataContext = vm;
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 1",
Details = "Details of Train 1"
});
vm.TrainViewModel.Trains.Add(new Train
{
Name = "Train 2",
Details = "Details of Train 2"
});
}
The TrainDetails controls would look like this, of course with more elements for more properties of the Train class:
<UserControl ...>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Details}"/>
</StackPanel>
</UserControl>
and the parent UserControl like this, where I directly use a ListBox instead of a TrainList control:
<UserControl ...>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding Trains}"
SelectedItem="{Binding SelectedTrain}"
DisplayMemberPath="Name"/>
<local:TrainDetailsControl Grid.Column="1" DataContext="{Binding SelectedTrain}"/>
</Grid>
</UserControl>
It would be instantiated in the MainWindow like this:
<Grid>
<local:TrainControl DataContext="{Binding TrainViewModel}"/>
</Grid>
Note that in this simple example the elements in the UserControls' XAML bind directly to a view model instance that is passed via their DataContext. This means that the UserControl know the view model (or at least their properties). A more general approach is to declare dependency properties in the UserControl class, that are bound to view model properties. The UserControl would then be independent of any particular view model.

NotifyPropertyChanged and ContentControl

I am struggling for one week now with my problem and i cant solve it. I am programming a MVVM WPF application which is having one window (MainView). In This Mainview i want to load different UserControl when i need them. In the Application-Startup I am loading the MainViewModel. In the Constructor of the MainViewModel I am loading the First ViewModel (LoginViewModel). Cause of my MainView.xaml it is showing me my Login-User-Control like i want to. So till this point everything is fine. In the ActivePanel-class i am saving the CurrentView, because in my MainView.xaml i am making a binding to my CurrentView for the ContentControl. So everything is working except the changing of the views although my NotifyPropertyChanged method of the CurrentView is working. I am thinking, that my mistake is in the xaml (DataBinding). Hope you guys can help me.
This is my MainView.xaml in which i want to load the different DataTemplates. Like I said before: The loading of the LoginViewModel via the Constructor of MainViewModel is working. The changing to other VieModels is working as well, but the DataBinding to the ContentControl is the big problem here.
<Window x:Class="App.View.MainView"
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:App.View"
mc:Ignorable="d"
xmlns:viewmodels="clr-namespace:App.ViewModels"
xmlns:views="clr-namespace:App.View"
xmlns:helper="clr-namespace:App.Helper"
Title="Betrooms" Height="500" Width="350">
<Window.Resources>
<DataTemplate x:Name="LoginUCTemplate" DataType="{x:Type viewmodels:LoginViewModel}">
<views:LoginUC DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate x:Name="RegUCTemplate" DataType="{x:Type viewmodels:RegViewModel}">
<views:RegUC DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate x:Name="HomeUCTemplate" DataType="{x:Type viewmodels:HomeViewModel}">
<views:HomeUC DataContext="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<viewmodels:ActivePanel/>
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentView, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
</Grid>
</Window>
This is the class of my ActivePanel where i am saving the information about which ViewModel is the active one. The CurrentView is the property I am binding the Content Control to.
namespace APP.ViewModels
{
public class ActivePanel : NotifyPropertyChanged
{
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
if (value != _currentView)
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
}
}
}
This is my MainViewModel:
namespace App.ViewModels
{
public class MainViewModel : ActivePanel
{
private LoginViewModel _loginViewModel;
public MainViewModel()
{
_loginViewModel = new LoginViewModel();
CurrentView = _loginViewModel;
}
}
}
And this is my LoginViewModel where I am changing the value of CurrentView via an action:
namespace App.ViewModels
{
public class LoginViewModel : ActivePanel
{
#region Member
private string _username;
private string _password;
bool login = false;
private HomeViewModel _homeViewModel;
private RegViewModel _regViewModel;
UserModel User = new UserModel();
#endregion
#region Properties
public ICommand RegActionCommand { get; set; }
public ICommand LogActionCommand { get; set; }
public string GetUsername
{
get { return _username; }
set
{
if (value != _username)
{
_username = value;
OnPropertyChanged("GetUsername");
}
}
}
public string GetPassword
{
get { return _password; }
set
{
if (value != _password)
{
_password = value;
OnPropertyChanged("GetPassword");
}
}
}
#endregion
#region Constructor
public LoginViewModel()
{
this.RegActionCommand = new RelayCommand(RegAction);
this.LogActionCommand = new RelayCommand(LogAction);
}
#endregion
#region Button-Action
private void LogAction(object obj)
{
_homeViewModel = new HomeViewModel();
CurrentView = _homeViewModel;
}
private void RegAction(object obj)
{
_regViewModel = new RegViewModel();
CurrentView = _regViewModel;
}
#endregion
}
}
I hope my question is understandable: The ContenControl binding is set to CurrentView but the ContentControl is never changing although the property of CurrentView is changing.
Thanks to you all. Cheers, Paul.
In your command handler, you are changing the CurrentView property of your LoginViewModel. The ContentControl's DataContext is the MainViewModel though, so it's content is bound to the CurrentView property of the MainViewModel.
You need to set the MainViewModel's property in your command handler. There are different ways of achieving this, for example you could add a constructor parameter to the LoginViewModel to pass a reference to the MainViewModel. You can save this reference and then access it in your command handler.
Another possibilty would be to raise an event or send a message from the command in your LoginViewModel and handle it in the MainViewModel. This would reduce the coupling between your ViewModels, but depending on which mechanism and library you use it might be a little bit more complicated.

WPF - How to keep Dependency Property and View Model property in sync?

In WPF I have been trying to figure out how to keep a views dependency property and one of it's view model's properties in sync for a while now without any luck. I have done a fair amount research into the subject but none of the suggested solutions are working for me and I was hoping someone could help me find what I am missing.
I attempted many of the things suggested in this post, Twoway-bind view's DependencyProperty to viewmodel's property?, because of all the things I read it looked to be the most promising, but was never able to get the results I was looking for.
I have written a simple program to demonstrate that issue I am having. In it I set the the property IntValue in MainWindowViewModel to 2 and then Bind it to a dependency property created in the UserControl IncrementIntView. Then when I push the button in IncrementIntView it increases the value of IntValue by one. This all works fine inside the UserControl IncrementIntView but I can't figure out how to send the updated IntValue back to MainWindowViewModel, it stays set to 2.
IncrementIntView.xaml.cs
public partial class IncrementIntView : UserControl
{
public int IntValue
{
get { return (int)GetValue(IntValueProperty); }
set { SetValue(IntValueProperty, value); }
}
public static readonly DependencyProperty IntValueProperty =
DependencyProperty.Register("IntValue", typeof(int), typeof(IncrementIntView),
new PropertyMetadata(-1, new PropertyChangedCallback(IntValueChanged)));
private static void IntValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
IncrementIntView detailGroup = dependencyObject as IncrementIntView;
if (e.NewValue != null)
{
detailGroup.ViewModel.IntValue = (int)e.NewValue;
}
}
public IncrementIntView()
{
InitializeComponent();
}
}
IncrementIntViewModel.cs
public class IncrementIntViewModel : ViewModelBase
{
private int intValue;
public int IntValue
{
get { return intValue; }
set { SetProperty(ref intValue, value); }
}
public IncrementIntViewModel()
{
incrementIntCommand = new Command(IncrementInt);
}
private Command incrementIntCommand;
public Command IncrementIntCommand { get { return incrementIntCommand; } }
public void IncrementInt()
{
IntValue++;
}
}
IncrementIntView.xaml
<UserControl.DataContext>
<local:IncrementIntViewModel x:Name="ViewModel" />
</UserControl.DataContext>
<Grid>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding IntValue}" />
<Button Content="Increment" Command="{Binding IncrementIntCommand}" Width="75" />
</StackPanel>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
MainWindowViewModel.cs
public class MainWindowViewModel : ViewModelBase
{
private int intValue = 2;
public int IntValue
{
get { return intValue; }
set { SetProperty(ref intValue, value); }
}
}
MainWindow.xaml
<Window.DataContext>
<local:MainWindowViewModel x:Name="ViewModel"/>
</Window.DataContext>
<Grid>
<StackPanel Margin="10">
<local:IncrementIntView IntValue="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ElementName=ViewModel}" />
<Label Content="{Binding IntValue}" />
</StackPanel>
</Grid>
I can see that your code is passing the IntValue from MainWindowViewModel's property to IncrementIntView's dependency property to IncrementIntViewModel's property.
The increment button is updating the IncrementIntViewModel's IntValue property. Unfortunately, whatever happens in the IncrementIntViewModel is not being reflected back to the IncrementIntView's IntValue dependency property. The TwoWay Mode is not between IncrementIntView's dependency property and IncrementIntViewModel's property, but it is between MainWindowViewModel's property to IncrementIntView's dependency property.
The easy solution: Bind the MainWindow's Label to IncrementIntViewModel's IntValue property without bothering the View's property.
<local:IncrementIntView x:Name="iiv" IntValue="{Binding IntValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ElementName=ViewModel}" />
<Label Content="{Binding DataContext.IntValue, ElementName=iiv}" />
<!--You need to specify DataContext.IntValue, because you have same name for both view's dependency property and viewmodel's property-->
Here you can see that MainWindowViewModel's IntValue is not that important, because it just passes the value to IncrementIntViewModel once and never have the value updated ever.
The other solution: You need to trigger value change back to MainViewModel's property.
First thing first, there is no connection between MainViewModel and IncrementIntViewModel. One solution is to make the MainViewModel to be singleton, so that when increment is done inside the IncrementIntViewModel, you want to update the MainViewModel's property as well.
In MainViewModel.cs
public static MainWindowViewModel SingletonInstance { get; set; }
public MainWindowViewModel()
{
if (SingletonInstance == null)
{
SingletonInstance = this;
}
}
In IncrementIntViewModel.cs
public void IncrementInt()
{
IntValue++;
MainWindowViewModel.SingletonInstance.IntValue = IntValue;
}
The other other solution: Similar to the above solution, but we don't need to make Singleton instance of MainWindowViewModel, because MainWindow is singleton to begin with.
In IncrementIntViewModel.cs
public void IncrementInt()
{
IntValue++;
((MainWindowViewModel)App.Current.MainWindow.DataContext).IntValue = IntValue;
}
If your intention is to update the IntValue from IncrementViewModel's property to IncrementView's dependency property, then you might ask why you need to do this, because MVVM is supposed to separate between V and VM. V should be looking to VM, but not the other way around.

WPF Binding ICommand with ViewModel

ICommand:
public class CMDAddEditUser : ICommand
{
public event EventHandler CanExecuteChanged;
public VMAddEditUser ViewModel { get; set;}
public CMDAddEditUser()
{
}
public CMDAddEditUser(VMAddEditUser vm)
{
ViewModel = vm;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
this.ViewModel.SimpleMethod();
}
}
ViewModel:
public class VMAddEditUser
{
private Employee _employee = new Employee();
private CMDAddEditUser Command { get; set; }
public VMAddEditUser()
{
Command = new CMDAddEditUser(this);
}
public string txtFirstName
{
get { return _employee.FirstName; }
set { _employee.FirstName = value; }
}
public void SimpleMethod()
{
txtFirstName = "abc";
}
}
XAML:
<Window x:Class="WPF.AddEditUserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ViewModel;assembly=ViewModel"
Title="AddEditUserView" Height="392.329" Width="534.143">
<Grid Margin="0,0,2,-3">
<Grid.Resources>
<vm:VMAddEditUser x:Key="abx"/>
</Grid.Resources>
<Grid.DataContext>
<vm:VMAddEditUser/>
</Grid.DataContext>
<Button x:Name="btn" Content="Cancel" Command="{Binding SimpleMethod, Source={StaticResource abx}}"/>
</Grid>
</Window>
The CMDAddEditUser and VMAddEditUser is in the same project while the xaml is in a different project.
The .Execute(Object Parameter) of the ICommand doesn't seem to work. I can't bind the SimpleMethod with the button that I have. When I type the Command Binding in the xaml file, the auto-complete/suggestions only shows the txtFirstName and not the SimpleMethod. I can't figure out why the SimpleMethod can't be binded and can't be found. What did I do wrong in this code?
First: All properties you want your view to be able to bind to, must be public. Since view binds to property, which is instance of ICommand implementation, property must be public, so view can access it. However, your SimpleMethod() can be private if you don't wanna expose it to the outside world, that why you have command calling it instead of letting view directly call it.
Second: You set you grids DataContext to your 'VMEditUser' class, so in binding there is no need to specify Source, DataContext is source.

Categories

Resources