Navigate through UserControl with MVVM LIGHT (WPF) - c#

First of all I apologize for my poor english which is not my first language.
I'm new in MVVM so my question is probably a very newbie one ;)
I'm encountering some issue with switching View in a C# Application using WPF and MVVM LIGHT. I've read a lot of articles but i still can't figured out how to do it in a clean way.
So here is my question: What is the best way to achieve the navigation between UserControl contained in a MainWindows, assuming that:
I've a ViewModel for each UserControl and one for the Main Windows.
The buttons for switching between usercontrols are contained into UserControl itself
I've a ViewModelLocator
I need to sometimes Destroy/re-create a userControl's ViewModel
I want to respect the MVVM Pattern.
I want to keep it simple

Since nobody answers to my question, this is what I finally did.
It might not be the best way but at least it works well.
I hope it'll helps some newbies like me who are struggling learning this pattern:
Put a CurrentViewModel Object in the MainViewModel:
public class MainViewModel : ViewModelBase,IMainViewModel
{
/* Other piece of code */
private ViewModelBase _currentViewModel;
public ViewModelBase CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
_currentViewModel = value;
RaisePropertyChanged(() => CurrentViewModel);
}
}
}
Obviously bind this to the Mainview (Just the relevant code):
<UserControl Content="{Binding Path=CurrentViewModel}"/>
Put the DataTemplate in the App.xaml:
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
<DataTemplate DataType="{x:Type localViewModel:HomeViewModel }">
<localView:AccueilView/>
</DataTemplate>
<DataTemplate DataType="{x:Type localViewModel:ErrorViewModel }">
<localView:ErrorView/>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
Register the ViewModel with Simple IOC in the ViewModelLocator:
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<IHomeViewModel, DesignHomeViewModel>();
}
else
{
SimpleIoc.Default.Register<IHomeViewModel, HomeViewModel>();
}
Set the getter of all the ViewModel in the ViewModelLocator to Static (just one for the exemple)
public static IHomeViewModel Home
{
get{return ServiceLocator.Current.GetInstance<IHomeViewModel>();}
}
Since it's static you can access the ViewModel you want from the MainViewModel:
public class MainViewModel : ViewModelBase,IMainViewModel
{
public ViewModelBase HomeVM
{
get
{
return (ViewModelBase)ViewModelLocator.Home;
}
}
}
Provide the ability to unregister the ViewModel and recreates it:
public static void CleanUpHome()
{
SimpleIoc.Default.Unregister<HomeViewModel>();
SimpleIoc.Default.Register<IHomeViewModel, HomeViewModel>();
}
The "child" View Model communicates with the MainViewModel through messages:
public class ErrorViewModel : ViewModelBase, IErrorViewModel
{
/*Other piece of code */
public void HomeReturn()
{
var msg = new ChangeView(ChangeView.EnumView.Home);
Messenger.Default.Send<ChangeView>(msg);
ViewModelLocator.CleanUpErrors();
}
}
The MainViewModel Register to the message and processes it:
public class MainViewModel : ViewModelBase,IMainViewModel
{
public MainViewModel()
{
Messenger.Default.Register<ChangeView>(this, (action) => ReceiveMessage(action));
CurrentViewModel = HomeVM;
}
private void ReceiveMessage(ChangeView viewName)
{
switch (viewName.switchView)
{
case ChangeView.EnumView.Home:
CurrentViewModel = HomeVM;
break;
case ChangeView.EnumView.Error:
CurrentViewModel = ErrorVM;
break;
}
Messenger.Default.Unregister<ChangeView>(this, (action) => ReceiveMessage(action));
}
That's all.

Related

WPF TabControl with MVVM using Dependency Injection

I'm very new to WPF but quite experienced with .NET and C#. I am trying to create (what I though would be) a fairly simple CRUD admin desktop application for a website I plan on building.
WPF seems to be way more complicated than I expected it to be and after lots of Googling I've basically realised that everyone uses the MVVM pattern - fine. Now, with my existing .NET experience, I know I definitely want to to be using dependency injection. I've discovered that everything seems to be done within the ViewModel in WPF, including all the services and everything - fine again.
Now, onto my problem. I have set up a basic tab control and I'm binding the tab values to an enum using Enum.GetValues(). I want the view to change when I select a tab and the view will depend on which tab is selected. The problem is, I can't seem to get the view to show - it just shows a blank screen. The view is a custom UserControl I've created and defined as a resource and contains a grid and a bunch of buttons and stuff. I've omitted this from below as it doesn't seem relevant.
My MainWindow.xaml is pretty simple and looks like this:
<Window x:Class="Stc.Admin.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:viewmodels="clr-namespace:Stc.Admin.ViewModels"
xmlns:views="clr-namespace:Stc.Admin.Views"
xmlns:local="clr-namespace:Stc.Admin"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TabControl ItemsSource="{Binding Tabs}" SelectedItem="{Binding CurrentTab}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:GamesViewModel}">
<views:Games />
</DataTemplate>
</TabControl.Resources>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding DataContext.CurrentViewModel, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TabControl}}}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Here's my MainViewModel.cs:
public class MainViewModel
{
private readonly IViewModelFactory<GamesViewModel> _gamesViewModelFactory;
private ViewType _currentTab;
public ViewType CurrentTab
{
get
{
return _currentTab;
}
set
{
_currentTab = value;
ChangeView(_currentTab);
}
}
public ObservableCollection<ViewType> Tabs { get; }
public ViewModelBase CurrentViewModel { get; set; }
public MainViewModel(IViewModelFactory<GamesViewModel> gamesViewModelFactory)
{
_gamesViewModelFactory = gamesViewModelFactory;
Tabs = new ObservableCollection<ViewType>(Enum.GetValues(typeof(ViewType)).Cast<ViewType>().ToArray());
}
private void ChangeView(ViewType viewType)
{
switch (viewType)
{
case ViewType.Games:
CurrentViewModel = _gamesViewModelFactory.CreateViewModel();
break;
case ViewType.Listings:
break;
case ViewType.Users:
break;
case ViewType.Languages:
break;
case ViewType.Currencies:
break;
default:
break;
}
}
}
public enum ViewType
{
Games,
Listings,
Users,
Languages,
Currencies
}
GamesViewModel has service dependencies so it needs to be created using the factory.
And my DI setup in App.xaml.cs:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
IServiceProvider serviceProvider = this.createServiceProvider();
Window window = new MainWindow();
window.DataContext = serviceProvider.GetRequiredService<MainViewModel>();
window.Show();
base.OnStartup(e);
}
private IServiceProvider createServiceProvider()
{
IServiceCollection services = new ServiceCollection();
services.AddDbContext<StcContext>(options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Stc;Integrated Security=True"));
services.AddSingleton<ICrudService<Game>, CrudService<Game>>();
services.AddSingleton<IViewModelFactory<GamesViewModel>, GamesViewModelFactory>();
services.AddScoped<MainViewModel>();
return services.BuildServiceProvider();
}
}
I have sorted this issue now. Being new to WPF, I didn't realise that I have to use INotifyPropertyChanged to get the UI to update after changing a property value on my ViewModel. I'd seen this used in a lot of the articles and tutorials I was seeing but didn't really understand what it was or how to apply it to my application.
The change I made was to implement this interface on my base ViewModel like so:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I then change my MainViewModel to inherit from the base class and changed the setter of the CurrentTab property to call OnPropertyChanged (with the name of the property) after I've changed the view/viewmodel property:
private ViewType _currentTab;
public ViewType CurrentTab
{
get
{
return _currentTab;
}
set
{
_currentTab = value;
ChangeView(_currentTab);
OnPropertyChanged(nameof(CurrentViewModel));
}
}
I believe this is telling the UI that something has changed and it needs to redraw itself. Correct me if I'm wrong or if that's an oversimplification.

MvvmLightLibsStd10 and UWP

How can you create a binding between a ViewModel and a View?
In the past there was a Locater created in App.xaml and then on the view you had this:
DataContext="{Binding MainViewModel, Source={StaticResource ViewModelLLocator}}"
I can't even click in the Properties of the View and then create DataContext binding.
In recent versions of MVVM light they changed how ViewModelLocator works due to it taking a dependency on Microsoft.Practices.ServiceLocation and the former not being .NET Standard compliant. It now should use GalaSoft.MvvmLight.Ioc to locate the ViewModel using SimpleIoc.
Here's an example how I used it in a recent UWP project.
In App.xaml
private ViewModels.ViewModelLocator Locator => Application.Current.Resources["Locator"] as ViewModels.ViewModelLocator;
In MainPage.xaml
DataContext="{Binding MainViewModel, Source={StaticResource Locator}}">
In MainPage.cs
private MainViewModel ViewModel
{
get { return DataContext as MainViewModel; }
}
In ViewModelLocator.cs
namespace YourNamespace.ViewModels
{
public class ViewModelLocator
{
public ViewModelLocator()
{
Register<MainViewModel, MainPage>();
}
public MainViewModel MainViewModel => SimpleIoc.Default.GetInstance<MainViewModel>();
public void Register<VM, V>()
where VM : class
{
SimpleIoc.Default.Register<VM>();
NavigationService.Configure(typeof(VM).FullName, typeof(V));
}
}
}
Ok I found it out:
You need to add this in App.xaml:
private static ViewModelLocator _locator;
public static ViewModelLocator Locator => _locator ?? (_locator = new ViewModelLocator());
And then in the View.xaml:
this.DataContext = App.Locator.MainViewModel;

WPF MVVM Child ViewModel properties not updating view

So I have a WPF app utilizing the MVVM pattern which uses the MVVM Light library. For INotifyPropertyChanged I am using the Fody.Weavers library which is working well so far.
I have a MainWindow.xaml which has a ContentControl, its Content property is bound to a property on its view model for navigation. This works well also.
MainWindow.xaml:
<ContentControl Content="{Binding SelectedViewModel}"></ContentControl>
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
// Note that Fody library handles INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public object SelectedViewModel { get; set; }
public MainViewModel()
{
SelectedViewModel = new HomeViewModel();
}
public RelayCommand<PasswordBox> AdminLoginCommand => new RelayCommand<PasswordBox>(AdminLogin);
private void AdminLogin(PasswordBox passwordBox)
{
// Login Logic...
// Does not work
SelectedViewModel = new HomeViewModel();
// Does not work either
if (SelectedViewModel is HomeViewModel)
{
((HomeViewModel)SelectedViewModel).CheckAccess();
}
// Does not work either
if (SelectedViewModel is HomeViewModel)
{
((HomeViewModel)SelectedViewModel).CanAccessTestButton = true;
}
}
}
However when I call the CheckAccess method on the SelectedViewModel, directly change the CanAccessTestButton property from MainViewModel, or set SelectedViewModel with a new HomeViewModel from MainViewModels AdminLogin method, they get updated as I see when I step through the code, but the binding does not update the UI. I have tried these methods independently.
I think Home.xaml is not picking up on the property when changed from the parent view model. When the constructor of HomeViewModel is initialized on first load it binds correctly to whatever CanAccessTestButton is set to, any other calls from MainViewModel do not seem to update the view.
Funnily enough, when I try to change the property from within the HomeViewModel using a RelayCommand bound to a another button in Home.xaml, it works fine.
How can I get it to work from the parent?
Home.xaml:
<Button Content="Test" IsEnabled="{Binding CanAccessTestButton}"/>
HomeViewModel.cs:
public class HomeViewModel : ViewModelBase
{
// Note that Fody library handles INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public bool CanAccessTestButton { get; set; }
public HomeViewModel()
{
OtherButtonCommand = new RelayCommand(OtherButtonClick);
CheckAccess();
}
public RelayCommand OtherButtonCommand { get; set; }
private void OtherButtonClick()
{
// WORKS!!!
CheckAccess()
}
public void CheckAccess()
{
CanAccessTestButton = AppContext.Instance.LoggedInUserHasAccess();
}
}
Edit:
MVVM Light has a ViewModelLocator.cs class which you need to declare each ViewModel in:
public class ViewModelLocator
{
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
SimpleIoc.Default.Register<HomeViewModel>();
}
public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
public HomeViewModel Home => ServiceLocator.Current.GetInstance<HomeViewModel>();
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
Then in each View, you reference the view model you want to bind to in (markup simplified for brevity)
MainWindow.xaml:
<Window DataContext="{Binding Main, Source={StaticResource Locator}}">
Home.xaml:
<UserControl DataContext="{Binding Home, Source={StaticResource Locator}}">
On startup, HomeViewModel constructor gets called twice, first from ViewModelLocator.cs, then again from MainViewModel.cs. Maybe the ViewModelLocator has the reference to the view that I see on screen. Any ideas on how to accomplish what I wish to achieve?
I noticed the HomeViewModel constructor gets called once from ViewModelLocator, then again within MainViewModel when the Content binding is set
That's your problem. You are creating another instance of the view model and bind to this one.
Remove the DataContext attribute from the UserControl in Home.xaml:
<UserControl DataContext="{Binding Home, Source={StaticResource Locator}}">
You want to bind to the HomeViewModel instance that you create yourself, not the one that the view model locator creates for you. So you don't need any view model locator here.

How to bind the DataContext of the Window or its child to one specific property of the Window?

A bit silly question, but somehow I can't find how to bind the DataContext of the Window or its Content (e.g a Grid panel) to one specific property of the Window (say, ViewModel in my example below):
Code:
internal partial class MyWin : Window
{
public MyViewModelType ViewModel { get; set; }
...
}
XAML:
<Window x:Class="MyNs.MyWin"
...
DataContext="{Binding RelativeSource={RelativeSource Self}}" />
<Grid DataContext={Binding ViewModel}> <!-- doesn't work??? -->
...
</Grid>
</Window>
I think you have this the wrong way around
if your window does the hooking up, it will work okay
public partial class MyWindow
{
public MyWindow()
{
InitializeComponent();
DataContext = ViewModel = new MyViewModelType();
}
}
Please define field for the viewmodel as it is not changing and implement INPC
private MyViewModelType viewmodel;
public MyViewModelType ViewModel
{
get
{
if(viewmodel == null)
{
viewmodel = new MyViewModelType();
}
return viewmodel;
}
set
{
viewmodel = value;
OnPropertyChanged("ViewModel")
}
}
Rest of code remains the same.

ContentControl not updating

I'm trying to have a MainWindow that is bound to the a view. I change that view in code and expect it to update in the Main Window, however that is not happening.
I have this code in my XAML
<Grid>
<ContentControl Content="{Binding Source={StaticResource ViewModelLocator}, Path=MainWindowViewModel.CurrentControl}" />
</Grid>
I then change my Control via this code
public class MainWindowViewModel : ReactiveObject
{
private UserControl _CurrentControl = null;
public UserControl CurrentControl
{
get
{
if (_CurrentControl == null)
{
_CurrentControl = new HomePage();
}
return _CurrentControl;
}
set
{
this.RaiseAndSetIfChanged(x => x.CurrentControl, value);
}
}
}
As you can see I'm using the ReactiveUI library.
Is ContentControl the wrong thing to use in that view or am I just not binding and updating correctly?
There is actually a far better way to do this, using ViewModelViewHost:
<Grid DataContext="{Binding ViewModel, ElementName=TheUserControl}">
<ViewModelViewHost ViewModel="{Binding CurrentControlViewModel}" />
</Grid>
Now, your class will look something like:
public class MainWindowViewModel : ReactiveObject
{
private ReactiveObject _CurrentControlViewModel = new HomePageViewModel();
public ReactiveObject CurrentControlViewModel {
get { return _CurrentControl; }
set { this.RaiseAndSetIfChanged(x => x.CurrentControlViewModel, value); }
}
}
And somewhere in your app's startup, you should write:
RxApp.Register(typeof(IViewFor<HomePageViewModel>), typeof(HomePage));
What's ViewModelViewHost?
ViewModelViewHost will take a ViewModel object that you provide via Bindings, and look up a View that fits it, using Service Location. The Register call is how you can associate Views with ViewModels.
why you call your class MainWindowViewModel? when you wanna do mvvm you shouldn't have properties with type UserControl in your VM.
the usual mvvm way looks like this:
viewmodel with INotifyPropertyChanged
public class MyViewmodel
{
public IWorkspace MyContent {get;set;}
}
xaml content control with binding to your VM
<ContentControl Content="{Binding MyContent}"/>
datatemplate --> so that wpf knows how to render your IWorkspace
<DataTemplate DataType="{x:Type local:MyIWorkSpaceImplementationType}" >
<view:MyWorkspaceView />
</DataTemplate>
I think you have several muddled concepts here and they are getting in each others way.
Firstly you aren't actually using ANY of the reactiveUI code, it never gets called. Since your get accessor implements a lazy instantiation pattern then it means the set accessor is ignored. This means that the view never notifies the property change, so you never get updates.
I'd recommend using something more like
private UserControl _currentControl;
public MainWindowVirwModel()
{
CurrentControl = new HomePage();
}
public UserControl CurrentControl
{
get { return _curentControl;}
set { this.RaiseAndSetIfChanged(...); }
}
In addition, this still mixes up View components i.e. HomePage, inside your ViewModel tier which will making unit testing far more difficult.

Categories

Resources