I've started writing my first WPF/MVVM and as many other people have found, dealing with navigation between views is rather confusing.
I've been searching for a while and most of the topics either recommend using MVVM Light/PRISM, or come up with solutions similar to one from here.
I'm trying to approach an MVVM navigation mechanism in which I can switch directly from one view to another view (without using the datatemplate switch from the parent window). Let's say, I have an app with a main window loading dynamic content from different usercontrols (views).
The MainWindowViewModel would have a CurrentV property pointed to, say, UserListV and a CurrentVM property pointed to UserListVM. Now that I select one user from the list and click on the View button to view that user details in another screen of the same window. This should allow me to switch to the UserV with UserVM as data context.
I wonder how should I, while being on UserListVM, make a call to MainWindowViewModel to update the CurrentV and CurrentVM values, and switching the window to the UserV accordingly?
Any suggestion of a better idea is more than welcome!
Thank you very much!
I use a messaging service for communication between ViewModels, while still keeping them decoupled. If you are using MVVMLight, it comes with one. I prefer not to use an MVVM framework, and write my own messaging service. Here is an example of one from a recent project:
public class MessageService : IMessageService
{
private List<IMessageSubscription> subscribers; //list of subscription objects registered
public MessageService()
{
subscribers = new List<IMessageSubscription>();
}
public void Subscribe<T>(string message, Action<T> action)
{
subscribers.Add(new MessageSubscription<T>()
{
Message = message,
MessageActionWithArgs = action
});
}
public void Subscribe(string message, Action action)
{
subscribers.Add(new MessageSubscription<bool>()
{
Message = message,
MessageActionNoArgs = action
});
}
public void Send<T>(string message, T args)
{
IEnumerable<IMessageSubscription> matches = subscribers.Where(x => x.Message == message && x.PayLoadType == typeof(T));
foreach (IMessageSubscription sub in matches.ToList())
{
sub.InvokeMessageAction((T)args);
}
}
public void Send(string message)
{
IEnumerable<IMessageSubscription> matches = subscribers.Where(x => x.Message == message);
foreach (IMessageSubscription sub in matches.ToList())
{
sub.InvokeMessageAction();
}
}
}
So, for example, MainViewModel would listen for a message such as "ActiveViewModelChangeRequest", and other viewmodels would send that message when they need to become active. So, in MainViewModel you would have something like this:
public MainViewModel()
{
messageService.Register<ViewModelBase>("ActiveViewModelChangeRequest", UpdateActiveViewModel);
}
private void UpdateActiveViewModel(ViewModelBase viewModel)
{
this.CurrentVM = viewModel;
}
And then in UserListVM you would have:
private void OnUserSelect(object sender, UserSelectionEventArgs e)
{
UserVM viewModel = new UserVM(SelectedUser);
messageService.Send<ViewModelBase>("ActiveViewModelChangeRequest, viewModel);
}
There's a lot of reading material available on the messenger pattern for MVVM applications. I would suggest reading up on this.
Related
I'm in a scenario to reuse a view with two completly independent view models.
For example you can think a generic list view to show apples somewhere and somewhere else to show cars. Doesn't really matter.
In Prism.Forms for Xamarin im able to glue a view with a viewModel like this.
Container.RegisterTypeForNavigation<PageA, ViewModelA>("PageA1");
Container.RegisterTypeForNavigation<PageA, ViewModelB>("PageA2");
I can't find an equivalent in Prism WPF, can someone help me out?
The link that #AdamVincent posted and the "missing" methods are very useful for normal view/viewmodel navigation using the ViewModelLocationProvider. However when trying to use two view models for the same view they don't work. This is because inside the extension method there is a call that registers the viewmodel to the view for use by the ViewModelLocationProvider.
private static IUnityContainer RegisterTypeForNavigationWithViewModel<TViewModel>(this IUnityContainer container, Type viewType, string name)
{
if (string.IsNullOrWhiteSpace(name))
name = viewType.Name;
ViewModelLocationProvider.Register(viewType.ToString(), typeof(TViewModel));
return container.RegisterTypeForNavigation(viewType, name);
}
Internally, ViewModelLocationProvider.Register uses a dictionary to store the association between view models and views. This means, whe you register two view models to the same view, the second will overwrite the first.
Container.RegisterTypeForNavigation<PageA, ViewModelA>("PageA1");
Container.RegisterTypeForNavigation<PageA, ViewModelB>("PageA2");
So with the above methods, when using the ViewModelLocationProvider, it will always create an instance of ViewModelB because it was the last one to be registered.
Additionally, the next line calls RegisterTypeForNavigation which itself ultimately calls Container.RegisterType, is only passing the viewType.
To resolve this, I tackled it a different way using an Injection property. I have the following method to bind my viewmodel to my view
private void BindViewModelToView<TView,TViewModel>(string name)
{
if (!Container.IsRegistered<TViewModel>())
{
Container.RegisterType<TViewModel>();
}
Container.RegisterType<TView, TViewModel>(name,new InjectionProperty("DataContext", new ResolvedParameter<TViewModel>()));
}
We know each view will have a DataContext property, so the Injection property will inject the viewmodel directly into the DataContect for the view.
When registering the viewmodels, instead of using RegisterTypeForNavigation, you would use the following calls:
BindViewModelToView<PageA,ViewModelA>("ViewModelA");
BindViewModelToView<PageA,ViewModelB>("ViewModelB");
To create the view, I already have a method that I use to inject the appropriate view into my region, and it works using the viewname as the key to obtain the correct viewmodel instance.
private object LoadViewIntoRegion<TViewType>(IRegion region, string name)
{
object view = region.GetView(name);
if (view == null)
{
view = _container.Resolve<TViewType>(name);
if (view is null)
{
view = _container.Resolve<TViewType>();
}
region.Add(view, name);
}
return view;
}
Which I simply call with
var view = LoadViewintoRegion<PageA>(region,"ViewModelA");
and
var view = LoadViewintoRegion<PageA>(region,"ViewModelB");
So for normal single View/Viewmodels, I use the ViewModelLocationProvider.AutoWireViewModel property and where I have multiple viewmodels, I use this alternative approach.
2022/01/15 Update
First of all thanks a lot for Jason's answer, his answer is great, and I implemented it perfectly with reference to his design, but because of Prism version update, I made some changes
I have an single view with multiple viewmodel
Step 1
register your view region
<ContentControl prism:RegionManager.RegionName="{x:Static hard:RegionNames.PanelPosCameraRegion}"></ContentControl>
Step 2
coding your viewmodel
private IRegionManager _RegionManager;
private IUnityContainer _UnityContainer;
public ICommand LoadedCommand { get; set; }
public RoboticPageVM(IRegionManager regionManager, IUnityContainer unityContainer)
{
_RegionManager = regionManager;
_UnityContainer = unityContainer;
LoadedCommand = new DelegateCommand(LoadedCommandHandle);
}
private void LoadedCommandHandle()
{
BindViewModelToView<PanelPosMultiplexView, PanelPosCameraVM>("Camera");
BindViewModelToView<PanelPosMultiplexView, PanelPosAxisVM>("Axis");
LoadViewIntoRegion<PanelPosMultiplexView>(RegionNames.PanelPosCameraRegion, "Camera");
LoadViewIntoRegion<PanelPosMultiplexView>(RegionNames.PanelPosAxisRegion, "Axis");
}
private void BindViewModelToView<TView, TViewModel>(string registerName)
{
if (!_UnityContainer.IsRegistered<TViewModel>())
{
_UnityContainer.RegisterType<TViewModel>();
}
_UnityContainer.RegisterType<TView>(registerName, new InjectionProperty(nameof(UserControl.DataContext), new ResolvedParameter<TViewModel>()));
}
private void LoadViewIntoRegion<TView>(string regionName, string registerName)
{
IRegion region = _RegionManager.Regions[regionName];
object? view = region.GetView(registerName);
if (view == null)
{
view = _UnityContainer.Resolve<TView>(registerName);
}
if (!region.Views.Any(v => v.GetType() == typeof(TView)))
{
region.Add(view, registerName);
}
}
Short version:
If I have ViewModel, containing its Model object and exposing its properties, how do I get the model "back" after it has been edited? If the Model-inside-ViewModel is public, it violates encapsulation, and if it is private, I cannot get it (right?).
Longer version:
I am implementing a part of an application which displays collections of objects. Let's say the objects are of type Gizmo, which is declared in the Model layer, and simply holds properties and handle its own serialization/deserialization.
In the Model layer, I have a Repository<T> class, which I use to handle collections of MasterGizmo and DetailGizmo. One of the properties of this repository class is an IEnumerable<T> Items { get; } where T will be some of the Gizmo subtype.
Now since Gizmo doesn't implement INPC, I have created the following classes in ViewModel layer:
GizmoViewModel, which wraps every public property of a Gizmo so that setting any property raises PropertyChanged accordingly;
[**] RepositoryViewModel<T>, which has an ObservableCollection<GizmoViewModel> whose CollectionChanged is listened to by a method that handles Adds, Removes and Updates to the repository.
Notice that the Model layer has a "Repository of Models", while the ViewModel layer has a "ViewModel with an ObservableCollection of ViewModels".
The doubt is related to the [**] part above. My RepositoryViewModel.CollectionChangedHandler method is as follows:
void CollectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var added in e.NewItems)
{
var gvm = added as GizmoViewModel;
if (gvm != null)
{
//// IS ANY OF THE ALTERNATIVES BELOW THE RIGHT ONE?
// Gizmo g = gvm.RetrieveModel(); ?? proper getter ??
// Gizmo g = GetModelFromViewModel(gvm); ?? external getter ??
// Gizmo g = gvm.Model; ?? public model property ??
_gizmo_repository.Add(g);
}
}
break;
....
Besides that, if anyone can detect any MVVM smell here, I'll be happy to know.
We can deal with our Models even outside the View and ViewModel layers, so leaving the model publicly accessible from ViewModel is I believe acceptable.
Let say you are creating the Models in "DataLayer" you can pass the instance of the Model to the ViewModel. To illustrate my point:
///Models ////////////////////////////
public interface IGizmo{}
public class Gizmo:IGizmo{}
public class SuperGizmo : IGizmo {}
public class SuperDuperGizmo : IGizmo { }
//////////////////////////////////////
public interface IGizmoViewModel<out T>
{
T GetModel();
}
public abstract class GizmoViewModelBase : IGizmoViewModel<IGizmo>
{
protected GizmoViewModelBase(IGizmo model)
{
_Model = model;
}
private readonly IGizmo _Model;
public IGizmo GetModel()
{
return _Model;
}
}
public class GizmoViewModel : GizmoViewModelBase
{
public GizmoViewModel(Gizmo model)
: base(model) { }
}
public class SuperDuperGizmoViewModel : GizmoViewModelBase
{
public SuperDuperGizmoViewModel(SuperDuperGizmo model)
: base(model){}
}
Your repository of Models will be updated on whatever updates it get from the ViewModel as long as you passed the same instance. So there is no need to have a repository of ViewModels to get the updates.
Reading your code, I think there is something of a mixup regarding your ViewModel and Model separation.
So, as I understand it, when your ObservableCollection of GizmoViewModel's changes, you are trying to add the Gizmo instance of the new item back to your Model?
I would approach this differently. You should create your Gizmo instances inside your Model layer, and when you do this you should add it to the Repository.
Otherwise, you haven't provided enough information - or rather, you have provided too much but it is the wrong sort of information. You need to describe the situation in which you want to do this, where these GizmoViewModels are created, etc.
From what I can see here, your GizmoViewModel has a dependency to your Repository<T>, so why not pass in the repository when you create your view model?
public class GizmoViewModel
{
private IRepository<Gizmo> _Repo;
//Underlying model (Doesn't implement INotifyPropertyChanged)
private Gizmo _Model;
//Wrapping properties
public int MyProperty
{
get { return _Model.Property; }
set
{
_Model.Property = value;
NotifyOfPropertyChange();
}
}
...
public GizmoViewModel(IRepository<Gizmo> repo)
{
_Repo = repo;
}
public void AddToRepo()
{
_Repo.Add(_Model);
}
...
It would be even better if these methods are inside the RepositoryViewModel base class. You can really go crazy with inheritance here. Perhaps something like this:
var gvm = added as IRepositoryViewModel;
if (gvm != null)
gvm.AddToRepo();
You can then simply call AddToRepo when you need to add the view model's underlying model to the repository.
Perhaps not the most elegant solution, however if encapsulation is what's worrying you, then you need to ensure that your dependencies are properly managed.
"If the Model-inside-ViewModel is public, it violates encapsulation"
Your assertion above is completely wrong and is killing your code.
By setting the Model property in ViewModel as private, you are forced to repeat your self ( code smells ), as you will need to define in your ViewModel, the same properties as you did for your Model, effectively transforming it into a Model class that mimics the Model it is supposed to expose to the View.
In MVVM the ViewModel role is to provide the View with all the presentation data and logic that it needs and for sure the Model is fundamental part of this data, by hidding it from the View you are killing MVVM.
I'm using WPF, MVVM and Entity Framework in my current project.
To keep things simple, let's say I have a viewmodel for CRUD operations towards a list of materials (Solid woods).
My ViewModel's EF context (WTContext) is initialized through property injection, for instance:
SolidWoods_VM newView = new SolidWoods_VM();
newView.Context = new WTContext(SettingsManager.Instance.GetConnectionString());
This way I'm able to test this ViewModel:
SolidWoods_VM swVM = new SolidWoods_VM();
swVM.Context = new FakeWTContext();
Imagine that during a insert operation something goes wrong and the WTContext.SaveChanges() fails.
What is the best way to refresh the ViewModels context?
Create a new bool property in the viewmodel named ForTestingPurposes, and when the SaveChanges method fails:
try
{
Context.SaveChanges();
}
catch
{
if (!ForTestingPurposes)
{
Context = new WTContext(SettingsManager.Instance.GetConnectionString());
}
}
Send a message to the mainviewmodel for context reloading (through mediator pattern):
Mediator.Instance.NotifyColleagues<SolidWoods_VM>(MediatorMessages.NeedToUpdateMyContext, this);
(Yet, this way I'd still need the bool property)
3.A more elegant solution, without aditional properties, provided for you guys :)
Why not abstract the methods/properties you need on your data context onto an interface and then have an implementation of that that handles the exception.
//WARNING: written in SO window
public interface IDataSource
{
void SaveChanges();
//... and anything else you need ...
}
public class RealDataSource : IDataSource
{
private WTContext _context;
public void SaveChanges()
{
try { _context.SaveChanges(); }
catch
{
_context = new WTContext(/*...*/);
}
}
}
This way you can still implement a fake/mock data source but your view model class doesn't need to know anything about how the data is actually retrieved.
My opinion is that your best bet would be the message.
You need a way to indicate that the save went wrong, and it might not serve all consumers of the class to have the context regenerated. If you're binding to your VM in there, for example, resetting the context might have other UI consequences.
I'm writing an application where I'm attempting to use an MVVM style architecture to handle my data binding (although I'm not using a MVVM specific library, such as MVVM Light). I've got a class which stores all of the information that my application requires, and then each of the screens is assigned a view model to its DataContext, which simply selects the values required for the specific screen, formatting the data if necessary.
As an example, the main data store looks something like this:
class DataStore {
int a, b, c;
string d;
DateTime e;
}
And then the view model allocated to a specific screen, which only uses several of the properties, is something like
class MainScreenViewModel {
public int data1 { get { return App.DataStore.a * App.DataStore.c } }
public int data2 { get { return App.DataStore.e.Day } }
}
This seems to work fine, when the page loads the data bindings are populated as they should be. However, they do not update automatically when the page loads. I've implemented INotifyPropertyChanged on the DataStore, but it seems that the change event doesn't bubble through to be reflected in the view model. I'm sure I'm going about this a really bad way, so if anyone could help point me in the right direction I'd be very grateful. I've read a stack of guides online, but I seem to be confusing myself more and more!
You have to implement INotifyPropertyChanged and raise PropertyChanged on your VM. In order to do this you will have to listen for DataStore.PropertyChanged. Sample:
class MainScreenViewModel {
public int data1 { get { return App.DataStore.a * App.DataStore.c } }
public int data2 { get { return App.DataStore.e.Day } }
public MainScreenViewModel()
{
App.DataStore.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == "a" || e.PropertyName == "c")
RaisePropertyChanged("data1");
if (e.PropertyName == "e")
RaisePropertyChanged("data2");
};
}
private void RaisePropertyChanged(string propertyName)
{
// raise it
}
}
The only part not covered here is the scenario when e.Day will change in DataStore.
Your approach itself is not the bad and is definitely good enough to start with.
You're binding to the MainScreenViewModel class, so it is that class that needs to implement INotifyPropertyChanged for the UI to get updated when the underlying data gets updated.
You could either move the logic into MainScreenViewModel and raise property change notification there, or handle the PropertyChanged event on DataStore in MainScreenViewModel and raise property changed notification for the appropriate properties.
I am writing an WPF MVVM application using Prism. A couple days ago I asked about best practices for managing different views and didn't get a whole lot of feedback. Sense then I have come up with a system that seems to work, but I want to make sure I wont get bit down the road.
I followed the tutorials at http://development-guides.silverbaylabs.org/ to get my shell setup and am confident that my modules are being registered well.
However, nowhere in those tutorials was an example of a view being replaced with a different view within a given region. This in general, seems to be fairly hard to find a good example of. So, today I rolled my own solution to the problem.
Essentially the module has a controller that keeps track of the current view, then when the user wants to switch views, it calls the Regions.Remove command and then the add command to replace it with the current view. It seems like there must be a more elegant solution to just switch between different registered views, but I haven't found it.
All the different possible views for a module are registered with the Unity container when the module is initialized.
The controller and the view switching function follows:
namespace HazardModule
{
public class HazardController : IHazardController
{
private object CurrentView;
public IRegionManager RegionManager { get; set; }
private IUnityContainer _container;
public HazardController(IUnityContainer container)
{
_container = container;
}
/// <summary>
/// Switches the MainRegion view to a different view
/// </summary>
/// <typeparam name="T">The class of the view to switch to</typeparam>
public void SiwthToView<T>()
{
if (CurrentView != null)
{
RegionManager.Regions["MainRegion"].Remove(CurrentView);
}
CurrentView = _container.Resolve<T>();
RegionManager.Regions["MainRegion"].Add(CurrentView);
}
}
}
Any feedback or other better solutions would be appreciated.
I have pretty much the same approach, so does a co-worker who has a bit more Prism experience than myself.
Basically I have a ViewController class which is a property in my ViewModelBase class. This enables all my ViewModels to have access to it in one go. Then in my ViewController class I have a few display management methods. The correctness of this approach is probably debatable but I found it to work quite well in my case
public TView ShowViewInRegion<TView>(string regionName, string viewName, bool removeAllViewsFromRegion)
{
var region = regionManager.Regions[regionName];
var view = region.GetView(viewName) ?? container.Resolve<TView>();
if (removeAllViewsFromRegion)
{
RemoveViewsFromRegion(region);
}
region.Add(view, viewName);
region.Activate(view);
if (regionName == RegionNames.OverlayRegion)
{
eventAggregator.GetEvent<PopupWindowVisibility>().Publish(true);
}
return (TView)view;
}
public void RemoveViewsFromRegion(string regionName)
{
RemoveViewsFromRegion(regionManager.Regions[regionName]);
}
private void RemoveViewsFromRegion(IRegion region)
{
for (int i = 0; i < region.Views.Count() + i; i++)
{
var view = region.Views.ElementAt(0);
region.Remove(view);
}
if (region.Name == RegionNames.OverlayRegion)
{
eventAggregator.GetEvent<PopupWindowVisibility>().Publish(false);
}
}
private static void DeactivateViewsInRegion(IRegion region)
{
for (var i = 0; i < region.ActiveViews.Count(); i++)
{
var view = region.ActiveViews.ElementAt(i);
region.Deactivate(view);
}
}
Then whenever I need to switch out a view or whatever I can just call from my ViewModel
public void ExecuteCreateUserCommand()
{
ViewController.ShowViewInRegion<IUserCreateView>(RegionNames.ContentRegion, ViewNames.UserCreateView, true);
}