I've got a behavior in my .net Core 3.1 WPF Application, which calls a command inside the ViewModel, after the view is displayed.
public class LoadedBehavior
{
public static DependencyProperty LoadedCommandProperty
= DependencyProperty.RegisterAttached(
"LoadedCommand",
typeof(ICommand),
typeof(LoadedBehavior),
new PropertyMetadata(null, OnLoadedCommandChanged));
private static void OnLoadedCommandChanged
(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is FrameworkElement frameworkElement && e.NewValue is ICommand)
{
frameworkElement.Loaded
+= (o, args) => { (e.NewValue as ICommand)?.Execute(null); };
}
}
public static ICommand GetLoadedCommand(DependencyObject depObj)
{
return (ICommand)depObj.GetValue(LoadedCommandProperty);
}
public static void SetLoadedCommand(
DependencyObject depObj,
ICommand value)
{
depObj.SetValue(LoadedCommandProperty, value);
}
}
This Behavior is attached inside the View:
behaviors:LoadedBehavior.LoadedCommand="{Binding LoadedCommand}"
I am working with Prisms RegionManager to inject my Views into specific areas inside the views. When I now try to inject a new view, the loaded command from the old view is called again. This seems like it comes from the bevavior.
For a better understanding, here is also the code which gets called to show a new view inside the specific region
public class NavigationService
{
private readonly IServiceLocator _serviceLocator;
private readonly IRegionManager _regionManager;
public NavigationService(IServiceLocator serviceLocator, IRegionManager regionManager)
{
_serviceLocator = serviceLocator;
_regionManager = regionManager;
}
public void Navigate(string regionName, object view)
{
RemoveAllViews(regionName);
_regionManager.AddToRegion(regionName, view);
}
public void Navigate<T>(string regionName) where T : FrameworkElement
{
var view = _serviceLocator.GetInstance<T>();
Navigate(regionName, view);
}
public void RemoveAllViews(string regionName)
{
_regionManager.Regions[regionName].RemoveAll();
}
}
Can anyone tell me, what I do wrong here? Or is this behavior not the way to go?
Edit
Right after posting this, I found the problem: The Loaded Command gets called multiple times. This seems to be caused by when the content of this view changes. So everytime I add a new view, the parent view calls it's loaded event. Is there a way to run the command only once the view is displayed?
The Loaded event is quite inreliable for triggering action with the intention of one time when the control is loaded. From the reference of the Loaded event for FrameworkElement.
Loaded and Unloaded might both be raised on controls as a result of user-initiated system theme changes. A theme change causes an invalidation of the control template and the contained visual tree, which in turn causes the entire control to unload and reload. Therefore Loaded cannot be assumed to occur only when a page is first loaded through navigation to the page.
In Prism you can act on navigation by creating a custom region behavior. In your example, you want to execute a command on a view model, once the view is added to a region. Create an interface that all your target view models implement with a command that should be executed when the view is displayed first.
public interface IInitializableViewModel
{
ICommand Initialize { get; }
}
Create a region behavior that watches the Views collection of a region and executes a command once, when a view is added to the region. It will check the data context of each view, if it implements the interface, the command is not null and command can execute.
public class InitializableDataContextRegionBehavior : RegionBehavior
{
public const string BehaviorKey = nameof(InitializableDataContextRegionBehavior);
protected override void OnAttach()
{
Region.Views.CollectionChanged += OnViewsCollectionChanged;
}
private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var frameworkElement in e.NewItems.OfType<FrameworkElement>())
{
if (frameworkElement.DataContext is IInitializableViewModel initializableViewModel &&
initializableViewModel.Initialize != null &&
initializableViewModel.Initialize.CanExecute(null))
{
initializableViewModel.Initialize.Execute(null);
}
}
}
}
}
Add the custom region behavior in your Prism application to the region behaviors collection.
protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
{
base.ConfigureDefaultRegionBehaviors(regionBehaviors);
regionBehaviors.AddIfMissing(InitializableDataContextRegionBehavior.BehaviorKey, typeof(InitializableDataContextRegionBehavior));
}
The command on each view model will execute exactly once, when the corresponding view is added to any region. Using an interface here was easier for demonstration purposes, but you can also create an attached property for your command that you attach to your view and bind to the view model.
Related
The problem is moving from the ServiceLocator anti-pattern to Dependency Injection. In view of my inexperience, I can't shift the DI principle to the code implemented now.
Summary
The Summary section is optional for read. You may want to comment on something, advise.
The main purpose of the program is the process of merging placeholder fields of specific information. Number of information makes need to have infrastructure around. Such as forms, services, database. I have some experience with this task. I managed to create a similar program based on WinForms. And it even works! But in terms of patterns, maintenance, extensibility, and performance, the code is terrible. It is important to understand that programming is a hobby. It is not the main education or job.
The experience of implementing the described task on WinForms is terrible. I started studying patterns and new platform. The starting point is UWP and MVVM. It should be noted that the binding mechanism is amazing.
The first problem on the way was solved independently. It is related to navigation in the UWP via the NavigationView located in the ShellPage connected with ShellViewModel. Along with creating a NavigationService. It is based on templates from Windows Template Studio.
Since there is a working WinForms program and its anti-pattern orientation, there is time and a desire to do everything correctly.
Now I'm facing an architecture problem. Called ServiceLocator (or ViewModelLocator). I find it in examples from Microsoft, including templates from Windows Template Studio. And in doing so, I fall back into the anti-pattern trap. As stated above, I don't want this again.
And the first thing that comes as a solution is dependency injection. In view of my inexperience, I can't shift the DI principle to the code implemented now.
Current implementation
The start point of app UWP is app.xaml.cs. The whole point is to transfer control to ActivationService. Its task is adding Frame to Window.Current.Content and navigated to default page - MainPage. Microsoft documentation.
The ViewModelLocator is a singleton. The first call to its property will call constructor.
private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());
// Constructor
private ViewModelLocator(){...}
Using ViewModelLocator with View (Page) is like this, ShellPage:
private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;
Using ViewModelLocator with ViewModel is similar, ShellViewModel:
private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;
Moving to DI
ShellViewModel has NavigationService from ViewModelLocator as shown above. How can I go to DI at this point? In fact, the program is small. And now is a good time to get away from anti-patterns.
Code
ViewModelLocator
private static ViewModelLocator _current;
public static ViewModelLocator Current => _current ?? (_current = new ViewModelLocator());
private ViewModelLocator()
{
// Services
SimpleIoc.Default.Register<NavigationService>();
// ViewModels and NavigationService items
Register<ShellViewModel, ShellPage>();
Register<MainViewModel, MainPage>();
Register<SettingsViewModel, SettingsPage>();
}
private void Register<TViewModel, TView>()
where TViewModel : class
where TView : Page
{
SimpleIoc.Default.Register<TViewModel>();
NavigationService.Register<TViewModel, TView>();
}
public ShellViewModel ShellViewModel => SimpleIoc.Default.GetInstance<ShellViewModel>();
public MainViewModel MainViewModel => SimpleIoc.Default.GetInstance<MainViewModel>();
public SettingsViewModel SettingsViewModel => SimpleIoc.Default.GetInstance<SettingsViewModel>();
public NavigationService NavigationService => SimpleIoc.Default.GetInstance<NavigationService>();
ShellPage : Page
private ShellViewModel ViewModel => ViewModelLocator.Current.ShellViewModel;
public ShellPage()
{
InitializeComponent();
// shellFrame and navigationView from XAML
ViewModel.Initialize(shellFrame, navigationView);
}
ShellViewModel : ViewModelBase
private bool _isBackEnabled;
private NavigationView _navigationView;
private NavigationViewItem _selected;
private ICommand _itemInvokedCommand;
public ICommand ItemInvokedCommand => _itemInvokedCommand ?? (_itemInvokedCommand = new RelayCommand<NavigationViewItemInvokedEventArgs>(OnItemInvoked));
private static NavigationService NavigationService => ViewModelLocator.Current.NavigationService;
public bool IsBackEnabled
{
get => _isBackEnabled;
set => Set(ref _isBackEnabled, value);
}
public NavigationViewItem Selected
{
get => _selected;
set => Set(ref _selected, value);
}
public void Initialize(Frame frame, NavigationView navigationView)
{
_navigationView = navigationView;
_navigationView.BackRequested += OnBackRequested;
NavigationService.Frame = frame;
NavigationService.Navigated += Frame_Navigated;
NavigationService.NavigationFailed += Frame_NavigationFailed;
}
private void OnItemInvoked(NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
NavigationService.Navigate(typeof(SettingsViewModel));
return;
}
var item = _navigationView.MenuItems.OfType<NavigationViewItem>().First(menuItem => (string)menuItem.Content == (string)args.InvokedItem);
var pageKey = GetPageKey(item);
NavigationService.Navigate(pageKey);
}
private void OnBackRequested(NavigationView sender, NavigationViewBackRequestedEventArgs args)
{
NavigationService.GoBack();
}
private void Frame_Navigated(object sender, NavigationEventArgs e)
{
IsBackEnabled = NavigationService.CanGoBack;
if (e.SourcePageType == typeof(SettingsPage))
{
Selected = _navigationView.SettingsItem as NavigationViewItem;
return;
}
Selected = _navigationView.MenuItems
.OfType<NavigationViewItem>()
.FirstOrDefault(menuItem => IsMenuItemForPageType(menuItem, e.SourcePageType));
}
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw e.Exception;
}
private bool IsMenuItemForPageType(NavigationViewItem item, Type sourcePageType)
{
var pageKey = GetPageKey(item);
var navigatedPageKey = NavigationService.GetNameOfRegisteredPage(sourcePageType);
return pageKey == navigatedPageKey;
}
private Type GetPageKey(NavigationViewItem item) => Type.GetType(item.Tag.ToString());
Update 1
Am I wrong about the equality between ServiceLocator and ViewModelLocator?
Called ServiceLocator (or ViewModelLocator)
Essentially, the current task is to connected View and ViewModel. NavigationService is beyond the scope of this task. So shouldn't it be in ViewModelLocator?
As #Maess notes, the biggest challenge you face (right now) is refactoring the static dependencies into constructor injection. For example, your ShellViewModel should have a constructor like:
public ShellViewModel(INavigationService navigation)
Once you have done that, you can then set up a DI framework (like NInject) with all your dependencies (kind of like your SimpleIoC thing), and, ideally, pull one root object from the container (which constructs everything else). Usually that's the main view model of the application.
I've done this successfully on multiple projects, both WPF and UWP, and it works great. Only thing you have to be careful of is when creating view models at runtime (as you often do), do it by injecting a factory.
I really made a search for this topic and did not find anything, and because of that, I am asking the question here.
I have a WPF application with Prism installed.
I have wired the view-model with the view automatically by name convention
<UserControl x:Class="Views.ViewA"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
and the model in the 'Model' like this
public class ViewAViewModel {
public ViewAViewModel () {
// time-resource consuming operations
}
}
the automatic binding work perfectly without a problem and the view and its corresponding view-model is matching, but the problem here.
I have a lot of those views say (50) and for every one of them, the view-model will be created with constructor exhausting the processes. This will make the startup of the application longer and also it will create a lot of view-models objects and put them in the RAM without being sure that they will be used at all.
What I need is to create the view-model class when the view is activated (I mean when the view is navigated to). Is this possible and if yes how?
Update
here is how I register the view with the Module, this is causing all the views to be created when the startup of the module.
public class Module1 : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("region1", typeof(View1));
regionManager.RegisterViewWithRegion("region1", typeof(View2));
is there any way to delay the creating of the views, until the navigation request come?
You could use navigation, for each view.
Or you must create an interfaces for your view and view model.
An example:
public interface IMyView
{
IMyViewModel ViewModel { get; set; }
}
public interface IMyViewModel
{
}
In the module or app.cs, in the method RegisterTypes you should register these.
containerRegistry.Register<IMyView, MyView>();
containerRegistry.Register<IMyViewModel, MyViewModel>();
You must implement IMyView interface in your MyView.cs class.
public partial class MyView : UserControl, IMyView
{
public MyView(IMyViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
}
public IMyViewModel ViewModel
{
get => DataContext as IMyViewModel;
set => DataContext = value;
}
}
After you could use it:
public void OnInitialized(IContainerProvider containerProvider)
{
var regionManager = containerProvider.Resolve<IRegionManager>();
var firstView = containerProvider.Resolve<IMyView>();
regionManager.AddToRegion(RegionNames.MainRegion, firstView);
}
In such case you shouldn't use ViewModelLocator.AutoWireViewModel in your view.
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?
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.
I have a ListBox on a Window. This Window's model uses Caliburn with a conductor:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
My screens are user controls (each UC has a Model) that gets loaded when I click a tab on TabControl.
I want to be able to access the ListBox selected item on all my screens.
How do I do that?
I can use EventAggreagator
In my AppBootstraper class I can add:
private CompositionContainer _container;
And under the Config method:
var batch = new CompositionBatch();
batch.AddExportedValue<IWindowManager>(new WindowManager());
batch.AddExportedValue<IEventAggregator>(new EventAggregator());
batch.AddExportedValue(_container);
_container.Compose(batch);
In my main ViewModel's constructor:
IEventAggregator eventAggregator
And in the bound property:
public Thing SelectedThing
{
get { return _selectedThing; }
set
{
_selectedThing = value;
NotifyOfPropertyChange(() => SelectedThing);
_eventAggregator.PublishOnUIThread(SelectedThing);
}
}
Then on my screen models:
public class MyScreenViewModel : Screen, IHandle<Thing>
In the constructor:
IEventAggregator eventAggregator
Then:
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
Interface implementation:
void IHandle<Thing>.Handle(Thing selectedThing)
{
this.SelectedThing = selectedThing;
}
More about it: Caliburn Micro Part 4: The Event Aggregator
The other option (although more coupled) is to use the fact that Screens have a "Parent" property which you can use to access their conductor; so you could do something like the following in the MyScreenViewModel.
void GetSelectedThing()
{
var conductingVM= this.Parent as ShellViewModel ;
this.SelectedThing = conductingVM.SelectedThing;
}