WPF MVVM Child ViewModel properties not updating view - c#

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.

Related

MVVM - How can I refer to child ViewModel from parent ViewModel (ViewModels are created in their Views)?

I don't know what is the correct way to create a viewmodel relationship if I use the approach that Views create instances of ViewModel and ViewModel has no reference to View.
Suppose we have ChildView control that creates an instance of its ViewModel that has SaveCommand.
<UserControl x:Class="App.ChildView" ...>
<UserControl.DataContext>
<local:ChildViewModel/>
</UserControl.DataContext>
<!-- some controls -->
</UserControl>
public class ChildViewModel
{
public RelayCommand SaveCommand { get; set; }
public ChildViewModel()
{
SaveCommand = new RelayCommand(SaveExecute);
}
private void SaveExecute()
{
Debug.WriteLine("Child data has been saved.");
}
}
Now, I put two controls in the parent view and want to execute SaveCommand on all children.
<Window x:Class="App.ParentView" ...>
<Window.DataContext>
<local:ParentViewModel/>
</Window.DataContext>
<Grid>
<local:ChildView x:Name="child1"/>
<local:ChildView x:Name="child2"/>
<Button Content="Save All" Command="{Binding ParentSaveCommand}">
<Grid/>
</Window>
public class ParentViewModel
{
public RelayCommand ParentSaveCommand { get; set; }
public ParentViewModel()
{
ParentSaveCommand = new RelayCommand(ParentSaveExecute);
}
private void ParentSaveExecute()
{
Debug.WriteLine("Saving all has started...");
// <childVM1>.SaveCommand.Execute();
// <childVM2>.SaveCommand.Execute();
// From where should I get ChildViewModel?
}
}
How correctly should I refer to child's ViewModel?
I found possible solutions:
Add an interface to the ParentView that returns the child's ViewModel (like AngelSix did with the password)
Create a class that will be able to bind the ChildView.DataContext to ParentView.DataContext.Child1ViewModel property.
.
Or maybe it's the wrong approach and ParentViewModel should create a ChildViewModel instances, and than ParentView should set DataContext for child1 and child2 (by binding of course)?
I mostly always assign DataContext in code behind so I can inject dependencies to view model constructor. My approach is to pass parent view model to child view models via constructor. In the case you have I would create a SaveAll event in parent view model and have child view models subscribe to it.
Edit: I don't use any framework for what I mentioned.
To give you a general idea on how I would handle SaveAll in Child view models -
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Child1ViewModel = new Child1ViewModel(this); // inject parent view model to child view model
//Child2ViewModel = new Child2ViewModel(this);
}
public event SaveAllEventHandler SaveAll; // Child view models can subscribe to this event
// ...
}
public class Child1ViewModel : ViewModelBase
{
public Child1ViewModel(MainViewModel parentViewModel)
{
parentViewModel.SaveAll += OnSaveAll;
}
private void OnSaveAll(object sender, SaveAllEventArgs e)
{
//
}
}
I hope that clarifies it a bit :)
Edit2: Like I had mentioned in my original response, for this to work you would need to set the DataContexts in MainWindow's constructor in code behind (and not in xaml).
Like RTF mentioned, for these types of operations it is generally an easier approach to have the parent VM create the children VMs so that the parent can maintain a reference to the children VMs. However, you can definitely do what you want without architectural changes.
In the view:
<Button
Content="Save All"
Height="37" Command="{Binding SaveAllCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource PassThroughConverter }">
<Binding Path=“DataContext” ElementName="child1"/>
<Binding Path=“DataContext” ElementName="child2"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
The Converter:
public class PassThroughConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.ToList();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
In parent view model:
public class ParentViewModel
{
public RelayCommand<object> ParentSaveCommand { get; set; }
public ParentViewModel()
{
this.ParentSaveCommand = new RelayCommand<object>(obj => this.ParentSaveExecute(obj));
}
private void ParentSaveExecute(object items)
{
foreach (var item in (ChildViewModel[])items)
{
item.SaveCommand.Execute();
}
}
}

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;

Navigate through UserControl with MVVM LIGHT (WPF)

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.

WPF Mvvm navigation with parameters

Following this tutorial (among others) and reading questions asked here I've constructed a navigation mechanism that will allow me to pass parameters between my ViewModels:
Object base - every view model inherits from it:
public abstract class ObjectBase : INotifyPropertyChanged
{
//INotifyPropertyChanged members
...
//Navigation handling
public abstract ObjectBase BackLocation { get; }
public abstract event Action<ObjectBase> NavigateTo;
public abstract string ViewHeader { get; }
}
MainViewModel - in charge of navigation:
public class MainViewModel : ObjectBase
{
private ObjectBase _selectedView;
private CommandBase _backCommand;
public MainViewModel()
{
SelectedView = new FirstViewModel();
}
public ObjectBase SelectedView
{
get { return _selectedView; }
set
{
SetProperty(ref _selectedView, value);
//register to the navigation event of the new view
SelectedView.NavigateTo += (target)=> { SelectedView = target; };
}
}
//This command is bound to a Back button on the main view
public CommandBase BackCommand
{
get { return _backCommand ?? (_backCommand = new CommandBase(Back)); }
}
private void Back(object obj)
{
if (SelectedView.BackLocation != null)
{
SelectedView = SelectedView.BackLocation;
}
else
{
Application.Current.Shutdown();
}
}
}
And the main view:
<Window ...
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type vm:FirstViewModel}">
<views:FirstView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:SecondViewModel}">
<views:SecondView/>
</DataTemplate>
</Window.Resources>
<ContentPresenter Content="{Binding SelectedView}"/>
</Window>
My problem is: If I set the DataTemplates in the main view like the above it makes each view aware of it's DataContext so if I want to add the DataContext explicitly to a view in order to use intellisense like this:
<UserControl x:Class="Wpf_NavigationTest.Views.FirstView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Wpf_NavigationTest.ViewModels">
<!--this causes the view model's constructor to get called again-->
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
<Grid>
<TextBlock Text="User control 1" FontSize="40"/>
</Grid>
the View Model's constructor is called twice, losing the parameters passed by the Navigate event.
The problem here is that you are setting the DataContext inside your UserControl, and also in your main view model.
<UserControl.DataContext>
<viewModels:FirstViewModel/>
</UserControl.DataContext>
The code above is instantiating a new FirstViewModel every time this UserControl is created. Therefore when the control gets created by the ContentControl (based on the DataTemplate), it then goes ahead and also creates a new FirstViewModel.
So, the solution here is to remove the UserControl.DataContext declaration in the UserControl, and you can instead set the DataContext of the ContentControl to that of your SelectedView.
<ContentPresenter Content="{Binding SelectedView}"
DataContext="{Binding SelectedView}"/>
In order to use multiple view models to a single view, you can simply add another DataTemplate:
<DataTemplate DataType="{x:Type vm:ThirdViewModel}">
<views:SecondView/>
</DataTemplate>
For Design-Time data (to get the intellisense), you can make use of d:DataContext as explained in this article.
This will require you to set up some view models as static resources, I would recommend creating them in a separate ResourceDictionary.

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