Background info
I'm developing a Xamarin Forms (v4.1.1.3, testing on iOS) application in XAML, using MVVM with a View first approach; I'm assigning single-instance ViewModels to Views by using the ViewModelLocator service of MVVMLight:
BindingContext="{Binding [SearchViewModel], Source={StaticResource ViewModelLocator}}"
When navigating to another page, I'm constructing a new instance of the page, which will receive the very same ViewModel instance every time.
var page = new SearchView();
var tabbedPage = Application.Current.MainPage as TabbedPage;
if (tabbedPage != null)
await tabbedPage.CurrentPage.Navigation.PushAsync(page);
The issue
I've implemented a custom control (view?), that is supposed to show search results in a tile-like layout. This control is created when navigating from a search NavigationPage to a search results ContentPage.
Every time I return to the search page and navigate back to search results, the view is reconstructed and the PropertyChanged of the BindableProperties are subscribed. These PropertyChanged events are never unsubscribed, so every time I navigate to the search results view and change the bound ViewModel property, the event is fired increasingly multiple times.
In the following code the OnItemsPropertyChanged is triggered multiple times, based on how many times I've navigated from the search view to the search results view:
public class WrapLayout : Grid
{
public static readonly BindableProperty ItemsProperty =
BindableProperty.Create("Items", typeof(IEnumerable), typeof(WrapLayout), null, propertyChanged: OnItemsPropertyChanged);
public IEnumerable Items
{
get { return (IEnumerable)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
public WrapLayout()
{
...
}
private static void OnItemsPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
...
}
}
My questions:
Shouldn't the BindableProperty unsubscribe from PropertyChanged and -Changing by itself?
Does this occur because of the way I associated Views with ViewModels and/or navigate through pages?
Should I handle unsubscribing these events myself, and how?
EDIT; additional navigation info
I have a MainView TabbedPage, which creates SearchView as NavigationPage:
public MainView()
{
InitializeComponent();
Children.Add(new NavigationPage(new SearchView())
{
Title = AppResources.Tab_Search,
Icon = "tab_search"
});
}
SearchView has, upon creation, a single-instance ViewModel assigned by the ViewModelLocator that was mentioned at the start of this topic, using MVVMLight's SimpleIoc container.
When a search command in SearchView is fired, I send a request to an API which returns search results. These results are displayed on another page, to which I navigate to from the SearchView's ViewModel:
await _navigationService.NavigateTo(ViewModelLocator.PageKeyFileResults, searchResult);
Which functionality looks somewhat like this:
public async Task NavigateTo(string pagekey, object viewModelParameter)
{
var constructor = _pagesByKey[pagekey].Constructor; //Gets the Func<Page> that simple creates the requested page, without using reflection.
var page = constructor() as Page;
var viewModel = page.BindingContext as BaseViewModel;
if (viewModel != null)
viewModel.Initialize(viewModelParameter);
var tabbedPage = Application.Current.MainPage as TabbedPage;
if (tabbedPage != null)
await tabbedPage.CurrentPage.Navigation.PushAsync(page);
else
await Application.Current.MainPage.Navigation.PushAsync(page);
}
The constructed page looks somewhat like:
<pages:BaseContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Views.FileResultsView"
xmlns:pages="clr-namespace:Views.Pages;assembly=Views"
xmlns:controls="clr-namespace:Views.Controls;assembly=Views"
BindingContext="{Binding [FileResultsViewModel], Source={StaticResource ViewModelLocator}}">
<ScrollView>
<controls:WrapLayout
Items="{Binding SearchResults}" />
</ScrollView>
</pages:BaseContentPage>
Where BaseContentPage is:
public class BaseContentPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<DialogMessage>(this, "ShowDialog", (dialogMessage) =>
{
if (string.IsNullOrWhiteSpace(dialogMessage.AcceptButton))
DisplayAlert(dialogMessage.Title, dialogMessage.Content, dialogMessage.CancelButton);
else
DisplayAlert(dialogMessage.Title, dialogMessage.Content, dialogMessage.AcceptButton, dialogMessage.CancelButton);
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<DialogMessage>(this, "ShowDialog");
}
}
And where ViewModel is basically like this:
public class FileResultsViewModel : BaseViewModel
{
private IEnumerable<ASRow> _searchResults;
public IEnumerable<ASRow> SearchResults
{
get { return _searchResults; }
set { Set(ref _searchResults, value); }
}
internal override void Initialize(object parameter)
{
base.Initialize(parameter);
if (parameter is AdvancedSearchResponse)
{
var searchResults = parameter as AdvancedSearchResponse;
SearchResults = new List<ASRow>(searchResults.Rows);
}
}
}
Shouldn't the BindableProperty unsubscribe from PropertyChanged and -Changing by itself?
Yes - it should. If it does not it is most certainly a bug
Does this occur because of the way I associated Views with ViewModels and/or navigate trough pages?
That is most likely also an option, since i didn't experience the behaviour you described yet. You would need to share more of your surrounding setup code.
Should I handle unsubscribing these events myself, and how?
It's hard for you to always control unsubscribing, since most of the time it will be the control subscribing to events (unless you do it yourself, in which case it's always your duty to unsub again)
While it is ugly it's sometimes necessary to get a quick workaround, which in your case would be browsing how xamarin holds a list of the change delegates and manually unsubscribe them on page appearing for example.
I hope that answers your question. Feel free to comment if it does not.
Update
In your case i would debug your page base, and verify wether or not
OnDisappearing is called correctly
Your handler is gone after unsubscribe
(This is lazy but i usually unsub an event before subbing it, just to make sure such a bug does not happen, because most EventManagement services won't throw if you're trying to unsub a handler which is not registered.)
at least that's the most likely causes of your issue.
Shouldn't the BindableProperty unsubscribe from PropertyChanged and -Changing by itself?
No. The Binding class takes care of this. Not the BindableProperty.
Does this occur because of the way I associated Views with ViewModels and/or navigate through pages?
You are seeing this because you are forgetting that the Navigation Stack keeps a list of pages in memory. Since multiple pages are pointing to the same BindingContext, there are multiple observers to changes. You would not have this particular issue if you didn't re-use View Models.
Should I handle unsubscribing these events myself, and how?
No. If it is really a concern then set BindingContext to null when a page disappears, and then restore it when reappearing. Keep in mind though that this still has cost to it, especially if your UI is really busy and has lots of dynamic content that is controlled by data bindings.
Related
I have a WPF application and i'm trying to respect the MVVM pattern rules. One of my views contains button:
<Button
Command="{Binding BrowseCommand}"
Margin="50, 0, 0, 0"
Style="{StaticResource CommonButtonStyle}"
Width="100"
Height="30">
<TextBlock
Text="Browse"/>
</Button>
Button command calls a method:
private void Browse(object sender)
{
DialogService.BrowseForDestinationPath(DestinationPath);
}
The main purpose of this method is to show "Select-directory-dialog", collect data and return it to the view model.
public static class DialogService
{
public static event Action<string> FolderBrowseRequested;
...
public static void BrowseForDestinationPath(string initialPath)
{
FolderBrowseRequested?.Invoke(initialPath);
}
}
Event defined in my DialogService class is invoked, and the subscriber method located in code-behind of the dialog fires:
protected void OnFolderBrowseRequested(string initialPath)
{
string destinationPath = initialPath;
var browsingDialog = new VistaFolderBrowserDialog();
if(browsingDialog.ShowDialog(this).GetValueOrDefault())
{
destinationPath = browsingDialog.SelectedPath;
var dataContext = DataContext as UnpackArchiveWindowViewModel;
if (dataContext != null)
dataContext.DestinationPath = destinationPath;
}
DialogService.FolderBrowseRequested -= OnFolderBrowseRequested; //so dumb
}
The problem is i really don't like this solution, I'm convinced it's unnecessarily complicated and inelegant. How to properly show a dialog on button click, collect some data and deliver it to our view model? I would like to keep View and ViewModel seperated and fully respect MVVM regime.
You could first start by describing the behavior your DialogService needs in an interface.
public interface IDialogService
{
void BrowseForDestinationPath(string initialPath);
event PathSelectedEvent PathSelected;
}
public delegate void PathSelectedEvent(string destinationPath);
Your ViewModel would contain a member of type IDialogService and subscribe to the PathSelectedEvent. The BrowseForDestinationPath method would be called using your Browse method which is called using the Command.
You could then create a user control which implements IDialogService. You could either inject this through your ViewModels constructor or if your ViewModel had a property like
public IDialogService FolderBorwser {get;set;}
the benefit of this approach is that all your view model knows about is an interface. You now delegate the responsibility of creating a concrete instance to something else. I would reccomend using an Injection Container like Unity or MEF as they handle the job of managing and resolving dependencies.
I encourage you to write your own logic because it helps you to understand the problem of opening dialogs in MVVM, but if you hit a brick wall or wan't to take the easy way out, there is a library called MVVM Dialogs that can help you with these problems.
Using this library you would write your code like this.
private void Browse(object sender)
{
var settings = new FolderBrowserDialogSettings
{
Description = "This is a description"
};
bool? success = dialogService.ShowFolderBrowserDialog(this, settings);
if (success == true)
{
// Do something with 'settings.SelectedPath'
}
}
I am trying to build a WPF Prism bases app using MVVM design pattern.
When my app first starts, I want to require the user to login. Once logged in, I want to show the default landing page with the user name and buttons.
My thought, when a user login, I would publish an event called UserLoggedIn then on the home view-model, I would listen for that event. When the event is triggered, I would show the landing/home view.
So I created the event like so
public class UserLoggedIn : PubSubEvent<User>
{
}
Then in the LoginViewModel I handle the login and publish the event like so
private void HandleLogin(LoginView loginView)
{
try
{
User user = AuthenticationService.Authenticate(Username, loginView.GetPassport());
IUserPassport passport = PassportManager.Get(user.Username);
if (passport == null)
{
// Create a new session
passport = new UserPassport(new CustomIdentity(user), RegionManager);
}
// Assign the current session
PassportManager.SetCurrent(passport);
// Trigger the event
EventAggregator.GetEvent<UserLoggedIn>().Publish(passport);
// Deactivate the login view
RegionManager.GetMainRegion().Deactivate(loginView);
}
catch (Exception ex)
{
//Do something with the error
}
}
Finally in my HomeViewModel aka my landing view, I have the following code to listen for the UserLoggedIn event.
public class HomeViewModel : BindableBase
{
protected IUnityContainer Container { get; set; }
protected ICoreRegionManager RegionManager { get; set; }
private IEventAggregator EventAggregator { get; set; }
public HomeViewModel(IUnityContainer container, ICoreRegionManager regionManager, IEventAggregator eventAggregator)
{
Container = container;
RegionManager = regionManager;
EventAggregator = eventAggregator;
eventAggregator.GetEvent<UserLoggedIn>().Subscribe(ShowTheLangingPage);
}
private void ShowTheLangingPage(User user)
{
var homeView = Container.Resolve<HomeView>();
RegionManager.AddToMainRegion(homeView);
FullName = user.FirstName;
}
// I am using PropertyChange.Fody package, so this propery will automaticly raise the PropertyChange event.
public string FullName { get; set; }
}
Problem is the ShowTheLangingPage method never get triggered in my HomeViewModel as expected.
I made sure the the View HomeView and HomeViewModel are wired up correctly, by directly loading the HomeView on module initialization for testing.
Additionally, if add Container.Resolve<HomeView>(); just before I publish the event, the ShowTheLangingPage I called. Its like I have to resolve the HomeView manually for it to listen for the event.
How can I correctly listen for the UserLoggedIn event so i can show the corresponding view.
So I can learn the better/recommended way, is it better to show the landing view from the LoginViewModel instead of using event/listener.... and why? Also, if showing the landing view directly from the LoginViewModel then what is the recommended method to navigate; using Region.Add() method or RegionManager.RequestNavigate?
is it better to show the landing view from the LoginViewModel instead of using event/listener....
Yes.
and why?
Because that's what services (like the IRegionManager) are for, doing stuff for your view models and other services. Also, you have noticed, events can only be subscribed to by living objects.
Also, if showing the landing view directly from the LoginViewModel then what is the recommended method to navigate; using Region.Add() method or RegionManager.RequestNavigate?
If anything, a third class should listen for UserLoggedIn, but that's no gain over using the IRegionManager directly. In fact, it's even worse, because you have to artifically create this class. Side note: if you wait for the garbage collector after Container.Resolve<HomeView>(); and before logging in, you won't go to the landing page, because there's no subscriber (again).
Most of my view models subscribe to a common event using Prism's EventAggregator on a WPF project. Basically, a vocal command triggers this event on a view, and as a response the view will publish another event containing its specific message to a text-to-speech module.
However, when I implemented this, I realized that when using RegionManager's RequestNavigate to switch to another view, the previous view model is still somehow active. When I trigger the common event for the most recent view, it is also triggered for the previous view.
Simplified example :
Start at View 1
Trigger common event
Response : message from View 1
RequestNavigate to View 2
Trigger common event
Response : message from View 2, then message from View 1
RequestNavigate to View 3
Trigger common event
Response : message from View 3, then View 2, then View 1
etc.
I placed a breakpoint on View 1, View 2 and View 3's common event, and each time I get a message from a view, its breakpoint is hit.
What I would like is simple : I don't want the previous ViewModel (and possibly View too) to be still somehow active when I'm switching Views. Even better would be for them to be garbage collected, because I also had some weird cases where by navigating to View 1, View 2 and View 1 again, the message for View 1 was sent twice (and its breakpoint also hit twice), so I'm not even sure if multiple references for the ViewModels are created, which could potentially lead to a memory leak.
I tried to reproduce this behavior by creating another project with just the essentials, so here's the code. I'm using Visual Studio 2017 with .net framework 4.5.2 and Ninject.
Shell.xaml
<Window x:Class="PrismTest.Shell"
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:prsm="http://prismlibrary.com/"
mc:Ignorable="d">
<Grid>
<ContentControl Name="MainRegion" prsm:RegionManager.RegionName="MainRegion" />
</Grid>
</Window>
NinjectPrismBootstrapper.cs
public class NinjectPrismBootstrapper : NinjectBootstrapper
{
protected override void InitializeModules()
{
base.InitializeModules();
// Text to speech
Kernel.Bind<SpeechSynthesizer>().ToSelf().InSingletonScope();
Kernel.Bind<INarrator>().To<StandardNarrator>().InSingletonScope();
Kernel.Bind<INarratorEventManager>().To<NarratorEventManager>().InSingletonScope();
// View models
Kernel.Bind<MainPageViewModel>().ToSelf();
Kernel.Bind<SecondPageViewModel>().ToSelf();
// Views
Kernel.Bind<object>().To<MainPageView>().InTransientScope().Named(typeof(MainPageView).Name);
Kernel.Bind<object>().To<SecondPageView>().InTransientScope().Named(typeof(SecondPageView).Name);
Kernel.Bind<Shell>().ToSelf();
var narratorEventManager = Kernel.Get<INarratorEventManager>();
var regionManager = Kernel.Get<IRegionManager>();
regionManager.RegisterViewWithRegion("MainRegion", typeof(MainPageView));
}
protected override DependencyObject CreateShell()
{
return (Shell)Kernel.GetService(typeof(Shell));
}
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
}
MainPageView.xaml (my starting page)
<UserControl x:Class="PrismTest.Views.MainPageView"
namespaces...>
<StackPanel>
<TextBlock Text="Main page"/>
<Button Content="Narrator speaks" Command="{Binding Path=NarratorSpeaksCommand}" />
<Button Content="Next page" Command="{Binding Path=GoToNextPageCommand}"/>
</StackPanel>
</UserControl>
MainPageView.xaml.cs
public partial class MainPageView : UserControl
{
public MainPageView(MainPageViewModel dataContext)
{
InitializeComponent();
this.DataContext = dataContext;
}
}
MainPageViewModel (View model for MainPageView)
public class MainPageViewModel : BindableBase, IRegionMemberLifetime, INavigationAware
{
private readonly IEventAggregator _eventAggregator;
private readonly IRegionManager _regionManager;
public DelegateCommand GoToNextPageCommand { get; private set; }
public DelegateCommand NarratorSpeaksCommand { get; private set; }
public MainPageViewModel(IEventAggregator eventAggregator, IRegionManager regionManager)
{
_eventAggregator = eventAggregator;
_regionManager = regionManager;
ConfigureCommands();
//The original common event triggered by a vocal command is simulated in this project by simply clicking on a button
_eventAggregator.GetEvent<CommonEventToAllViews>().Subscribe(NarratorSpeaks);
}
private void ConfigureCommands()
{
GoToNextPageCommand = new DelegateCommand(GoToNextPage);
NarratorSpeaksCommand = new DelegateCommand(ClickPressed);
}
private void GoToNextPage()
{
_regionManager.RequestNavigate("MainRegion", new Uri("SecondPageView", UriKind.Relative));
}
private void ClickPressed()
{
_eventAggregator.GetEvent<CommonEventToAllViews>().Publish();
}
private void NarratorSpeaks()
{
_eventAggregator.GetEvent<NarratorSpeaksEvent>().Publish("Main page");
}
}
I don't need to put the code for SecondPageViewModel and SecondPageView, because it's the exact same code except RequestNavigate sends the user back to MainPageView and its NarratorSpeaks method sends a different string.
What I tried :
1) Making MainPageViewModel and SecondPageViewModel inherit IRegionMemberLifetime and setting KeepAlive to false
2) Inheriting INavigationAware and returning false in IsNavigationTarget method
3) Adding this to OnNavigatedFrom method from INavigationAware :
public void OnNavigatedFrom(NavigationContext navigationContext)
{
var region = _regionManager.Regions["MainRegion"];
var view = region.Views.Single(v => v.GetType().Name == "MainPageView");
region.Deactivate(view);
}
Worth noting : even without the deactivate part, if I put a breakpoint after var region = _regionManager.Regions["MainRegion"]; and check region.views, there is only one result, no matter how much I switch views.
Nothing worked, events keep being triggered in previous views as I switch views back and forth.
So, I'm kind at a loss here. I'm not sure if it's my way of registering Views and ViewModels in Ninject that triggers this, or something else, but if someone has a suggestion, I'll gladly take it.
Thanks!
I had similar problems in the past. Have you considered unsubscribe from events when navigated from?
I'm trying to learn more about MVVM implementation in WPF and currently need some guidance on navigation using ViewModels. I'm following WPF navigation example from Rachel's blog and need a way to call Command of ApplicationViewModel from other ViewModel.
As per the blog, switching views from MainWindow is pretty clear, but I want to know more about inter-view navigation i.e. say I've Home, Product and Contact button on MainWindow along with View and ViewModel classes, now I want to open Contact page from some button inside Home view instead of MainWindow. I have written some code in Home ViewModel to achieve the same but I doubt whether this is the best practice of MVVM. And is there any way to achieve the same from HomeView.XAML?
Code Snippet from blog - ApplicationViewModel.cs
private ICommand _changePageCommand;
private IPageViewModel _currentPageViewModel;
private List<IPageViewModel> _pageViewModels;
public ApplicationViewModel()
{
// Add available pages in c'tor
PageViewModels.Add(new HomeViewModel(this));
PageViewModels.Add(new ProductsViewModel());
PageViewModels.Add(new ContactViewModel());
}
public ICommand ChangePageCommand
{
get
{
if (_changePageCommand == null)
_changePageCommand = new RelayCommand(
p => ChangeViewModel((IPageViewModel)p), p => p is IPageViewModel);
return _changePageCommand;
}
}
private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
PageViewModels.Add(viewModel);
CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
}
Code Snippet from blog - ApplicationView.xaml
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeViewModel}">
<local:HomeView />
</DataTemplate>
<!-- Data template for other views -->
</Window.Resources>
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand,
RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding }"/>
<!--All closing tags-->
My code inside HomeViewModel.cs
// This is the command to get bind with my button inside Home view to invoke Contact view
private ICommand _loadContactCommand;
public ICommand LoadContactCommand
{
get
{
if (_loadContactCommand == null)
_loadContactCommand = new RelayCommand(p => LoadOtherView());
return _loadContactCommand;
}
}
private void LoadOtherView()
{
// _appVM is the instance of 'ApplicationViewModel' which is being set from c'tor
// Even I'm thinking to pass Contact view member of ApplicationViewModel class here,
// as I need exactly the same instance of the Contact which has been created earlier
_appVM.ChangePageCommand.Execute(new ContactViewModel());
}
There's a couple of ways I'd do this.
The first, if the action is a service type of interaction, which I think this is a reasonably good example of, I would describe the action in an interface and inject it as a dependency into the ViewModels that need it.
This is effectively what you are doing, but it's worth abstracting it out into an interface. This provides less tight coupling between the two ViewModels.
Here is an example of wrapping up the functionality in an IPageDisplay interface:
public interface IPageDisplay
{
IPageViewModel GetCurrentPage();
void ChangeViewModel(IPageViewModel newPage);
}
Your ApplicationViewModel implements it and has the exact same methods it did before:
public class ApplicationViewModel: IPageDisplay
{
// implement like you are doing
You're HomeViewModel then takes as an interface, not the 'whole' ViewModel:
class HomeViewModel
{
HomeViewModel(IPageDisplay pageDisplay) {//constructor stuff}
private void LoadOtherView()
{
// Instead of interacting with a whole ViewModel, we just use the interface
_pageDisplay.ChangePageCommand.Execute(new ContactViewModel());
}
This is 'safer' as it's more abstract. You can test HomeViewModel without creating a AppViewModel by just mocking the IPageDisplay. You can change how pages are displayed or the implementation of AppViewModel, you can also display your pages in any other kind of location, by having some other implementation of IPageDisplay.
It's worth noting that any page that needs to perform navigation actions will require an IPageDisplay. It can be troublesome matching up all these dependencies if you have many of them - that's where something like a Dependency Injection framework can really help out.
The second would be a mediator pattern as suggested in the comments. You could have a common mediator PageManager that defines the ChangeViewModel(IPageViewModel newPage); method and fires a ChangeViewModelRequest event or callback. The ApplicationViewModel, and any other ViewModels that want to change the current page accept the PageManager instance as a dependency. ApplicationViewModel listens to the event, the other's call ChangeViewModelRequest to trigger it.
Again, a Dependency Injection will need to be managed effectively if this is in a complex application.
This naturally leads onto the third. Which is a extension of the mediator pattern, an Event Aggregator.
An event aggregator is a generic service that allows all different ViewModels to raise, or subscribe to application wide events. It's definitely worth looking at.
Here, your ApplicationViewModel subscribes to the event:
public class ApplicationViewModel
{
private EventAgregator _eventAggregator;
ApplicationViewModel(EventAgregator eventAggregator)
{
this._eventAggregator = eventAggregator;
_eventAggregator.Subscribe('ChangeViewModelRequest', (EventArgs eventArgs) => ChangeViewModel(eventArgs.Parameter))
}
private void ChangeViewModel(IPageViewModel viewModel)
{
if (!PageViewModels.Contains(viewModel))
PageViewModels.Add(viewModel);
CurrentPageViewModel = PageViewModels.FirstOrDefault(vm => vm == viewModel);
}
}
And the HomeViewModel publishes to the event:
private void LoadOtherView()
{
_eventAggregator.Publish("ChangeViewModelRequest", new EventArgs(new ContactViewModel()));
}
There are plenty of Event Aggregators you can use, some built into MVVM frameworks like Prism.
While, like all the others, this is a dependency - it's a very generic one. Chances are, most of your ViewModels will need access to the aggregator instance and have it as a dependency, as it could be used for almost all inter-view-model communication. Simply having all VMs pass it to any created VMs in the constructor could work for a simple application. But I'd still say something that supports dependency injection (say, factory pattern?) would be worth implementing.
Edit:
Here's what you need for your HomeViewModel:
public class HomeViewModel : IPageViewModel // doesn't implement IPageDisplay
{
private IPageDisplay _pageDisplay;
public HomeViewModel(IPageDisplay pageDisplay)
{
// HomeViewModel doesn't implement IPageDisplay, it *consumes* one
// as a dependency (instead of the previous ApplicationViewModel).
// Note, that the instance you're passing still is the ApplicationViewModel,
// so not much has actually changed - but it means you can have another
// implementation of IPageDisplay. You're only linking the classes together
// by the functionality of displaying a page.
_pageDisplay= pageDisplay;
}
public string Name
{
get
{
return "Home Page";
}
}
private ICommand _loadDashboardCommand;
public ICommand LoadDashboardCommand
{
get
{
if (_loadDashboardCommand == null)
{
_loadDashboardCommand = new RelayCommand(
p => LoadOtherView());
}
return _loadDashboardCommand;
}
}
private void LoadOtherView()
{
// Here you have the context of ApplicatiomViewModel like you required
// but it can be replaced by any other implementation of IPageDisplay
// as you're only linking the little bit of interface, not the whole class
_pageDisplay.ChangeViewModel(new DashboardViewModel());
}
}
}
I'm looking for the right way to dispose objects in a Xamarin Forms application. Currently i'm using XAML and MVVM coding style. Then from my view model i get a reference to a disposable object through the builtin service locator (DependencyService). Ideally i should be able to call Dispose() on the objects from my view model, but other solutions like attaching to ContentPage.OnDisappearing and NavigationPage.Popped could be feasible.
I had pretty much the same requirement a couple of weeks ago. I wanted to make sure that event subscriptions in my view models would be unsubscribed when the page is closed. After a lot of research my conclusion was that the simplest solution was to use the ContentPage.OnDisappearing method.
As you pointed out the object you want to dispose is in your ViewModel, so you need a little bit of infrastructure to make sure your ViewModel is informed when the it's disappearing. To do that I defined a base implementation of my view model that had two key methods OnAppearing and OnDisappearing (note this was a class rather than an interface because I have other base functionality such as IPropertyNotify implementation - not shown here).
public class ViewModelBase
{
/// <summary>
/// Called when page is appearing.
/// </summary>
public virtual void OnAppearing()
{
// No default implementation.
}
/// <summary>
/// Called when the view model is disappearing. View Model clean-up should be performed here.
/// </summary>
public virtual void OnDisappearing()
{
// No default implementation.
}
}
Then I subsclassed ContentPage and override the OnAppearing and OnDisappearing methods and then use them to notify my view model.
public class PageBase : ContentPage
{
/// <summary>
/// Performs page clean-up.
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
var viewModel = BindingContext as ViewModelBase;
// Inform the view model that it is disappearing so that it can remove event handlers
// and perform any other clean-up required..
viewModel?.OnDisappearing();
}
protected override void OnAppearing()
{
base.OnAppearing();
// Inform the view model that it is appearing
var viewModel = BindingContext as ViewModelBase;
// Inform the view model that it is appearing.
viewModel?.OnAppearing();
}
}
Then when you implement a page just make sure that it is of type PageBase:
<?xml version="1.0" encoding="utf-8" ?>
<pages:PageBase xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Forms.App.Controls;assembly=Forms.App"
xmlns:converters="clr-namespace:Forms.App.Converters;assembly=Forms.App"
xmlns:pages="clr-namespace:Forms.App.Pages;assembly=Forms.App"
x:Class="Forms.App.Pages.LogonPage"
NavigationPage.HasNavigationBar="False"
Title="Logon">
And in your ViewModel you can then override your OnDisappearing method and dispose your objects:
public class FormViewModel : ViewModelBase
{
public override void OnDisappearing()
{
base.OnDisappearing();
// Dispose whatever objects are neede here
}
}
Just one thing to watch out for - if you're using stack navigation the OnDisappearing method gets called when you stack another page on-top of your current page (your page is disappearing temporarily after all). So you will need to cater for this and probably not dispose your object in that case. However if you're not stacking anything on-top of your page there is nothing to worry about. In my case it was just event subscriptions so I attached the event handlers in the OnAppearing and detached them on the OnDisappearing.
I hope that helps you out!
We were getting disposed of object exceptions in Forms when Bindings to ListViews or Labels changed values as pages/fragments were being disposed of. I'm assuming you could dispose of objects in your ViewModel the same place we were removing bindings.
protected override void OnParentSet()
{
base.OnParentSet();
if (Parent == null)
{
//Clear a bunch of bindings or dispose of ViewModel objects
BindingContext =
_listView.ItemsSource = null;
}
}
I have View Models that conform to IDisposable. So I needed a way for the Page's BindingContext to be disposed when the page is no longer needed.
I used the suggestion of Nick which uses OnParentSet being set to NULL to known when the page is no longer needed.
The SafeContentPage class can be used in place of ContentPage. Iff The binding context supports IDisposable will it automatically try to dispose the binding context.
public class SafeContentPage : ContentPage
{
protected override void OnParentSet()
{
base.OnParentSet();
if (Parent == null)
DisposeBindingContext();
}
protected void DisposeBindingContext()
{
if (BindingContext is IDisposable disposableBindingContext) {
disposableBindingContext.Dispose();
BindingContext = null;
}
}
~SafeContentPage()
{
DisposeBindingContext();
}
}
The OnDisappearing method isn't a reliable technique as there are platform differences in terms of when it is called, and just because the page disappeared doesn't mean its View Model is no longer needed.