navigation to a new page removes backstack - c#

I have a strange recurring problem. Sometimes it goes away, other times it comes back. I can't pinpoint at all the issue, all my breakpoints seem to be hit in expected order.
When I navigate to a new page, my backstack keeps getting deleted, so pressing back just backgrounds the app. Obviously this is a problem.
I think it may be a result of my more complex page and viewmodel structures. I created a new class for all the NavigationHelper stuff for Pages enforcing that all my Pages subclass from the new class. I enforce that all my Pages attach themselves to a base PageViewModel class to resolve the communication between the two (I had a better way but Xaml doesn't play well), and I navigate using a NavigationService, where I call CurrentFrame, which is a static method for return Windows.Current.Content as Frame.
Here are what I think are relevant code. Any ideas? Thanks a bunch in advance. I have no clue what's going on :/
I navigate forward using the Navigate method in NavigationService (not the other two lolol), but my back button doesn't go back properly.
public abstract class BaseViewModelPage : Page
{
protected readonly NavigationHelper NavigationHelper;
protected BaseViewModelPage()
{
NavigationHelper = new NavigationHelper(this);
NavigationHelper.LoadState += navigationHelper_LoadState;
NavigationHelper.SaveState += navigationHelper_SaveState;
this.NavigationCacheMode = NavigationCacheMode.Required;
}
protected BasePageViewModel CurrentPageViewModel
{
get { return DataContext as BasePageViewModel; }
}
#region Navigation Registration
protected override void OnNavigatedTo(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
NavigationHelper.OnNavigatedFrom(e);
}
protected virtual void LoadState(LoadStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.LoadState(e);
}
}
protected virtual void SaveState(SaveStateEventArgs e)
{
if (CurrentPageViewModel != null)
{
CurrentPageViewModel.SaveState(e);
}
}
private void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
LoadState(e);
}
private void navigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
SaveState(e);
}
#endregion
}
public abstract class BasePageViewModel : ViewModelBase
{
private bool _isLoading = false;
public bool IsLoading
{
get
{
return _isLoading;
}
set
{
if (_isLoading == value)
{
return;
}
_isLoading = value;
RaisePropertyChanged();
}
}
public virtual void LoadState(LoadStateEventArgs e)
{
}
public virtual void SaveState(SaveStateEventArgs e)
{
}
}
public class NavigationService : INavigationService
{
public static readonly Dictionary<Type, Type> PageDictionary;
static NavigationService()
{
PageDictionary = new Dictionary<Type, Type>();
PageDictionary.Add(typeof(LogInPageViewModel), typeof(LogInPage));
PageDictionary.Add(typeof(RegisterUserPageViewModel), typeof(RegisterUserPage));
}
public bool Navigate(Type pageViewModelType, Object parameter = null)
{
if (PageDictionary.ContainsKey(pageViewModelType))
{
if (parameter != null)
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType], parameter);
}
else
{
return App.CurrentFrame.Navigate(PageDictionary[pageViewModelType]);
}
}
return false;
}
public bool GoBack()
{
if (CanGoBack())
{
App.CurrentFrame.GoBack();
}
return false;
}
public bool CanGoBack()
{
return App.CurrentFrame.CanGoBack;
}
public bool NavigateAndRemoveSelf(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
if (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
return true;
}
}
return false;
}
public bool NavigateAndRemoveAll(Type pageViewModelType, object parameter = null)
{
if (Navigate(pageViewModelType, parameter))
{
while (App.CurrentFrame.CanGoBack)
{
App.CurrentFrame.BackStack.RemoveAt(App.CurrentFrame.BackStackDepth - 1);
}
return true;
}
return false;
}
}
Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
and a second reliance on the Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add the WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!

Update [solved]:
The error is caused by using a Universal App Class Library.
I wanted to separate the NavigationHelper.cs class (generated by default in WP8 apps) into a library. so that I could unit test the VM directly (I could not reference the WP8 app with the Unit Test project). Thus, I placed the NavigationHelper.cs class, plus all my relevant code above, in a new Universal App Class Library.
The NavigationHelper class relies on two things, a WINDOWS_PHONE_APP macro in the BUILD, which affects this specific part in the NavigationHelper class, the HardwareButton BackPressed listener.
#if WINDOWS_PHONE_APP
Windows.Phone.UI.Input.HardwareButtons.BackPressed += HardwareButtons_BackPressed;
#else
...
#endif
Because the MACRO wasn't defined, the back button wouldn't actually go back.
A second problem was the missing Windows.Phone assembly. The assembly exists in a WP8 app, but not for a Universal App Class Library. This means that even if I add a WINDOWS_PHONE_APP macro to the library, the app will not compile. You cannot use the NavigationHelper generated by Windows Phone 8/8.1 projects inside a Universal App Class Library. I will try to raise this issue. Thanks!

You can leave your NavigationHelper in your shared project, just add this to your MainPage in the Windows Phone project..
static MainPage()
{
HardwareButtons.BackPressed += (sender, args) =>
{
var frame = Window.Current.Content as Frame;
if (frame != null && frame.CanGoBack)
{
frame.GoBack();
args.Handled = true;
}
};
}
This solved my BackButton issues.

Related

How to change the toolbar back icon in xamarin forms android

I am working on xamarin forms. I wanted to change the Toolbar back icon how to do it. I searched lot about it. I didn't get proper solution. Any help would be appreciated.
Thanks
praveen
Try this
LoadApplication(new App());
var upArrow = Resources.GetDrawable(Resource.Drawable.abc_ic_ab_back_mtrl_am_alpha);
upArrow.SetColorFilter(Resources.GetColor(Resource.Color.white), PorterDuff.Mode.SrcIn);
ActionBar.SetHomeAsUpIndicator(upArrow);
References
https://forums.xamarin.com/discussion/57791/cant-change-android-back-button-in-xamarin-forms
https://forums.xamarin.com/discussion/103317/change-navigation-bar-back-button-color-in-xamarin-android
How to change the toolbar back icon in xamarin forms android
You could refer to my answer: How to change navigation page back button in xamarin forms.
I write it here again:
We need to custom a NavigationPageRenderer, override the OnPushAsync method to set the Toolbar's navigation icon.
using AToolbar = Android.Support.V7.Widget.Toolbar;
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(NavigationPageRendererDroid))] // APPCOMP
...
public class NavigationPageRendererDroid : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer // APPCOMP
{
public AToolbar toolbar;
public Activity context;
protected override Task<bool> OnPushAsync(Page view, bool animated)
{
var retVal = base.OnPushAsync(view, animated);
context = (Activity)Xamarin.Forms.Forms.Context;
toolbar = context.FindViewById<Android.Support.V7.Widget.Toolbar>(Droid.Resource.Id.toolbar);
if (toolbar != null)
{
if (toolbar.NavigationIcon != null)
{
toolbar.NavigationIcon = Android.Support.V4.Content.ContextCompat.GetDrawable(context, Resource.Drawable.Back);
//toolbar.SetNavigationIcon(Resource.Drawable.Back);
}
}
return retVal;
}
}
The CustomNavigationPage are defined in PCL :
public class CustomNavigationPage : NavigationPage
{
public CustomNavigationPage(Page startupPage) : base(startupPage)
{
}
}
Usage :
public App()
{
InitializeComponent();
MainPage = new CustomNavigationPage(new MainPage());
}
...
// In MainPage
private async void Button_Clicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestPage());
}
[Effect].

WPF: dispatcher.invoke do not running

I write my first WPF application, which consist of several pages:
Welcome page with some logo
Login page with login form
Main page with account info
MainWindow contains <Frame> WPF Control, and I use animation to show next/previous page.
I write my own MainAnimation class to perform animation.
This application works fine on my laptop, but when I try to run it on the machine of my friend animation just do nothing.
I think that trouble related with Dispatcher.Invoke() method calling, and I tried to find solution over the web (here here here and here) and I tried:
use Application.Current.Dispatcher
use Dispatcher.BeginInvoke() instead of Dispatcher.Invoke()
but it does nothing.
So, I show Welcome page only 2 seconds and Login page must loaded automatically.
This is the code of WelcomePage.xaml.cs file:
public partial class WelcomePage : Page {
public WelcomePage (MainWindow parent) {
InitializeComponent();
this.parent = parent;
Task.Factory.StartNew(() => ShowLoginForm());
}
private MainWindow parent;
private void ShowLoginForm()
{
Thread.Sleep(2000);
this.parent.GoToLoginForm();
}
}
This is the code of MainWindow.xaml.cs file:
public partial class MainWindow : Window {
public MainWindow () {
InitializeComponent();
animation = new MainAnimation(this, this, Main, new WelcomePage(this));
}
private MainAnimation animation;
public void GoToLoginForm() => animation.ShowNextPage(new LoginPage(this));
public void GoToVideosForm() => animation.ShowNextPage(new MainPage(this));
}
And this is related parts on MainAnimation class (MainAnimation.cs):
public class MainAnimation
{
public MainAnimation(FrameworkElement resourcesOwner, DispatcherObject dispatcherOwner, Frame currentPageContainer, Page firstPage)
{
this.resourcesOwner = resourcesOwner;
this.dispatcherOwner = dispatcherOwner;
this.currentPageContainer = currentPageContainer;
pages = new Stack<Page>();
pages.Push(firstPage);
currentPageContainer.Content = pages.Peek();
}
private Stack<Page> pages;
private FrameworkElement resourcesOwner;
private DispatcherObject dispatcherOwner;
private Frame currentPageContainer;
private void ShowPageForward()
{
dispatcherOwner.Dispatcher.Invoke((Action)delegate {
if (currentPageContainer.Content != null)
{
var page = currentPageContainer.Content as Page;
if (page != null)
{
page.Loaded -= NextPage_Loaded;
UnloadPageForward(page);
}
}
else
{
LoadPageForward();
}
});
}
private void UnloadPageForward(Page page)
{
Storyboard sb = (resourcesOwner.FindResource("SlideForwardOut") as Storyboard).Clone();
sb.Completed += StoryboardForward_Completed;
sb.Begin(currentPageContainer);
}
private void StoryboardForward_Completed(object sender, EventArgs e)
{
LoadPageForward();
}
private void LoadPageForward()
{
pages.Peek().Loaded += NextPage_Loaded;
currentPageContainer.Content = pages.Peek();
}
private void NextPage_Loaded(object sender, RoutedEventArgs e)
{
Storyboard sb = resourcesOwner.FindResource("SlideForwardIn") as Storyboard;
sb.Begin(currentPageContainer);
}
}
I'm new with WPF and may be just don't understand some details, so I will be happy if you help me to solve this small but very offensive issue.
Update #1: software versions
OS for development: Windows 10 x64
OS for test: Windows 8.1 x64
VS version: Visual Studio 2017 Community Edition
Application target framework: v.4.5
Since WPF controls have thread affinity it doesn't make much sense to create them on a background thread in most cases.
If you want to wait for 2 seconds before you show the login page, you could either use a DispatcherTimer or wait asynchronously:
public partial class WelcomePage : Page
{
public WelcomePage(MainWindow parent)
{
InitializeComponent();
this.parent = parent;
ShowLoginForm();
}
private MainWindow parent;
private async void ShowLoginForm()
{
await Task.Delay(2000);
this.parent.GoToLoginForm();
}
}
Then you won't need any calls to Dispatcher.Invoke.

App crashes when Navigation from pages via back button

I am developing Windows Phone 8.1 app with MVVM.
I have base view model class which contains Navigation Service as below:
public abstract class BaseViewModel : INotifyPropertyChanged
{
protected readonly INavigationService NavigationService;
//....
}
There is my navigation service class:
public class NavigationService : INavigationService
{
public void Navigate(Type destinationPage)
{
((Frame)Window.Current.Content).Navigate(destinationPage);
}
public void Navigate(Type desitnationPage, object parameter)
{
((Frame)Window.Current.Content).Navigate(desitnationPage, parameter);
}
public void GoBack()
{
((Frame)Window.Current.Content).GoBack();
}
}
Everything is working fine when I am binding commands from XAML. There is problem when I want to override BackButton. I have also created base page model which also contains NavigationService. Each page has an overridde pf BackPressed as below:
public class BasePage : Page
{
protected INavigationService NavigationService => ComponentManager.GetInstance<INavigationService>();
public BasePage()
{
//...
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
}
protected virtual void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
//Frame.Navigate(typeof(MainPage));
(this.DataContext as BaseViewModel)?.Back.Execute(sender);
}
}
As you see in HardwareButtons_BackPressed method I've tried to make it in to ways but none is workings. Every time I press back button application crashes without any error.
I don't think the app is crashing, it's just exiting because that is the default behaviour of the back button.
What you need to do is flag that you've handled the back button by adding this line of code in your BackPressed event handler:
e.Handled = true;

caliburn.micro Condutor not working

We are using caliburn.micro for one of our projects and I'm currently having a puzzling problem:
we have the following classes:
ToolViewerViewModel : Conductor<Screen>.Collection.OneActive
DocViewerViewModel : Conductor<DocumentViewModel>
and various document-views, all with this base class:
DocumentViewModel : Screen
The ToolViewerViewModel is to manage multiple dock-able tool views which allow the user to control different aspects of the program.
The DocViewerViewModel is to show the user the data he's working on/with. It is here to present one of the many DocumentViewModel to the user. and is implemented as a special dock-able view which can not be closed or detached from the ToolViewerView. For every aspect of the data a specific DocumentViewModel is generated by the DocViewerViewModel and presented to the user.
The DocumentViewModel is the base class for all presentation aspects of the data. One may present the data as a table an other may present it as a chart, and so on...
We now encounter problems in terms of OnActivate() and OnDeactivate() which are not called when we expect them to be called.
First Problem:
The system is up and running; The DocumentViewModel is displayed in the DocViewerViewModel which is embedded in the ToolViewerViewModel along with one or two other dock-able views. The currently selected dock-able view is the DocViewerViewModel. When the user now selects one of the other dock-able views the OnDeactivate() method from the DocumentViewModel is being called. Which makes absolutely no sense to me. I'd expect the DocViewerViewModel.OnDeactivate() to be called.
Second Problem:
The system is up and running; The DocumentViewModel is displayed in the DocViewerViewModel which is embedded in the ToolViewerViewModel along with one or two other dock-able views. The currently selected dock-able view is the view that enables the user to change the DocumentViewModel presented by the DocViewerViewModel. When the user now selects an other DocumentViewModel the following code is being executed within the DocViewerViewModel:
DocViewerViewModel.DeactivateItem(oldDocumentViewModel, true);
DocViewerViewModel.ActivateItem(new DocumentViewModel());
I'd expect the DocumentViewModel.OnDeactivate() to be called upon the DocViewerViewModel.DeactivateItem(oldDocumentViewModel, true) call. but that never happens.
Conclusion:
The only proper working Conductor is the ToolViewerViewModel which is managing everything. But this behavior is not what we want or expect to happen: We'd like to have the ToolViewerViewModel only Conduct the dock-able views and the DocViewerViewModel to conduct the DocumentViewModel. This is important because there are two different use cases in place: One to manage multiple instances at the same time and the other where only one instance is active and used, the old instance shall be thrown away.
Hopefully anyone here can help me to get the behavior I'm looking for.
I Now have an example code for you:
public class ToolViewerViewModel : Conductor<Screen>.Collection.OneActive
{
private readonly IDockManager _dockManager;
private readonly DocViewerViewModel _docViewerViewModel;
private readonly IList<DockableViewModel> _toolViews = new List<DockableViewModel>();
public ToolViewerViewModel(IViewModelFactory viewModelFactory, DocViewerViewModel docViewerViewModel, IDockManager dockManager)
{
_dockManager = dockManager;
_viewModelFactory = viewModelFactory;
_docViewerViewModel = docViewerViewModel;
}
protected override void OnViewLoaded(object view)
{
_dockManager.Link(this);
_dockManager.CreateSpecialPaneFor(_docViewerViewModel);
ActivateItem(_docViewerViewModel);
ShowToolView<ProjectExplorerViewModel>();
base.OnViewLoaded(view);
}
public void ShowToolView<T>() where T : DockableViewModel
{
if (!IsToolViewOpen<T>())
{
var viewModel = _viewModelFactory.Create<T>();
ActivateItem(viewModel);
RefreshMenu(typeof(T));
}
}
}
Next class:
public class DocViewerViewModel : Conductor<DocumentViewModel>
{
private readonly IViewModelFactory _viewModelFactory;
public DocViewerViewModel(IViewModelFactory viewModelFactory)
{
_viewModelFactory = viewModelFactory;
}
public bool ShowInMainView<T>() where T : DocumentViewModel
{
return ShowInMainView(typeof(T));
}
private bool ShowInMainView(Type viewModelType)
{
var ret = false;
// close the current view
if (ActiveItem != null)
{
DeactivateItem(ActiveItem, true); //The close flag is on true since we want to remove the current instance from the memory
}
// check whether the current viewModel has been closed successfully
if (ActiveItem == null)
{
try
{
var viewModel = _viewModelFactory.Create(viewModelType) as DocumentViewModel;
if (viewModel != null)
{
ActivateItem(viewModel);
ret = true;
}
else
{
ActivateItem(_viewModelFactory.Create<NoDataViewModel>());
}
}
catch (Exception ex)
{
ActivateItem(_viewModelFactory.Create<NoDataViewModel>());
}
}
return ret;
}
}
and the last one:
public abstract class DocumentViewModel : Screen
{
private bool _isDirty;
protected IViewModelFactory ViewModelFactory { get; private set; }
protected IEventAggregator EventAggregator { get; private set; }
public bool IsDirty
{
get
{
return _isDirty;
}
protected set
{
if (value.Equals(_isDirty))
{
return;
}
_isDirty = value;
NotifyOfPropertyChange(() => IsDirty);
}
}
protected DocumentViewModel(IViewModelFactory viewModelFactory, IEventAggregator eventAggregator)
{
ViewModelFactory = viewModelFactory;
EventAggregator = eventAggregator;
}
protected override void OnDeactivate(bool close)
{
if (close)
{
if (EventAggregator != null)
{
EventAggregator.Unsubscribe(this);
}
}
base.OnDeactivate(close);
}
protected override void OnActivate()
{
if (EventAggregator != null)
{
EventAggregator.Subscribe(this);
}
base.OnActivate();
}
public override void CanClose(Action<bool> callback)
{
var ret = true;
if (IsDirty && (ViewModelFactory != null))
{
var saveDialog = ViewModelFactory.Create<SaveDialogViewModel>();
saveDialog.Show();
if (saveDialog.DialogResult == DialogResult.Cancel)
{
ret = false;
}
else
{
if (saveDialog.DialogResult == DialogResult.Yes)
{
Save();
}
else
{
Discard();
}
IsDirty = false;
}
}
callback(ret);
}
public abstract void Save();
public virtual void Discard()
{
}
}
With this code the only time the DocumentViewModel.OnDeactivate() is being called when the user brings an other dock-able view into focus while the DocViewerViewModel was having the focus. This should not happen!
When the user is changing the focus between the dock-able views the DocumentViewModel.OnDeactivate() should not get call. But it must get called when ever the Method DocViewerViewModel.ShowInMainView<SomeDocumentViewModel>() is being called. Which isn't the case currently.
As far as I can tell, there is nothing wrong with the way your code is written. Since you are using MVVM, I suggest you design a test case like I've provided here.
And here's a snippet of the test case
// TestHarness.cs
[TestMethod]
public void CheckDeactivation()
{
// We'd like to have the ToolViewerViewModel only Conduct the dock-able views
// and the DocViewerViewModel to conduct the DocumentViewModel.
IViewModelFactory factory = new ViewModelFactory();
DocViewerViewModel docViewer = new DocViewerViewModel(factory);
IDockManager dockManager = null;
var toolViewer = new ToolViewerViewModel(factory, docViewer, dockManager);
var mockToolView = new UserControl();
(toolViewer as IViewAware).AttachView(mockToolView);
DocumentViewModel docView1 = new NoDataViewModel();
DocumentViewModel docView2 = new NoDataViewModel();
docViewer.ActivateItem(docView1);
docViewer.ActivateItem(docView2);
Assert.AreEqual(0, docViewer.CountDeactivated());
}
I have had the exact same problem as you, and ended up using PropertyChangedBase instead of Screen and got the problem to disappear.
Later, after reading the docs on Screens and Conductors here, I realized that I wasn't activating the conductor itself further up in the view hierarchy!
So have a look at wherever you use your ToolViewerViewModel, and make sure you activate that instance!
Thank you very much for your Test. Even thought it is really nice code it tests the wrong code part. Your code simply tests whether the Method ActivateItem() or DeactivateItem() is being called:
public override void ActivateItem(DocumentViewModel item)
{
_countActivated++;
base.ActivateItem(item);
}
public override void DeactivateItem(DocumentViewModel item, bool close)
{
_countDeactivated++;
base.DeactivateItem(item, close);
}
But since these Methods are being called explicitly we don't need to test for that...
The real Problem is that the Conductor is not calling the OnActivate() or OnDeactivate() on the DocumentViewModel class. To enhance your test I used the following code:
public class DummyViewModelFactory : IViewModelFactory
{
private readonly Dictionary<Type, Func<object>> _registredCreators = new Dictionary<Type, Func<object>>();
public T Create<T>() where T : PropertyChangedBase
{
return Create(typeof(T)) as T;
}
public object Create(Type type)
{
if (type == null)
{
return null;
}
if (_registredCreators.ContainsKey(type))
{
return _registredCreators[type]();
}
return null;
}
public void Release(object instance)
{
}
public void RegisterCreatorFor<T>(Func<T> creatorFunction)
{
_registredCreators.Add(typeof(T), () => creatorFunction());
}
}
As concrete DocumentViewModel implementation I made:
public class NoDataViewModel : DocumentViewModel
{
public NoDataViewModel(IEventAggregator eventAggregator,
IViewModelFactory viewModelFactory)
: base(viewModelFactory, eventAggregator, )
{
}
public override void Save()
{
// nothing to do
}
public override void Reload()
{
// nothing to do
}
}
public class NoDataViewModelMock : NoDataViewModel
{
private static int activationCounterForTesting = 0;
private static int deactivationCounterForTesting = 0;
public static int ActivationCounterForTesting
{
get
{
return activationCounterForTesting;
}
}
public static int DeactivationCounterForTesting
{
get
{
return deactivationCounterForTesting;
}
}
public NoDataViewModelMock()
: base(null, null)
{
}
protected override void OnActivate()
{
activationCounterForTesting++;
base.OnActivate();
}
protected override void OnDeactivate(bool close)
{
deactivationCounterForTesting++;
base.OnDeactivate(close);
}
}
And I changed your Testmethod to this:
[TestMethod]
public void CheckDeactivation()
{
var viewModelFactory = new DummyViewModelFactory();
viewModelFactory.RegisterCreatorFor<NoDataViewModel>(() => new NoDataViewModelMock());
var docViewer = new DocViewerViewModel(viewModelFactory);
IDockManager dockManager = null;
var toolViewer = new ToolViewerViewModel(viewModelFactory, docViewer, dockManager);
var mockToolView = new UserControl();
(toolViewer as IViewAware).AttachView(mockToolView);
docViewerViewModel.ShowInMainView<NoDataViewModel>();
docViewerViewModel.ShowInMainView<NoDataViewModel>();
docViewerViewModel.ShowInMainView<NoDataViewModel>();
Assert.AreEqual(3, NoDataViewModelMock.ActivationCounterForTesting);
Assert.AreEqual(2, NoDataViewModelMock.DeactivationCounterForTesting);
}
Then you'll see that the OnActivate() and OnDeactivate() methods are never been called.
With a little more advanced test you'd also see that they are being called but from the ToolViewerViewModel directly. I'd like to know why and how I can change this behavior to fit my needs:
The DocumentViewModel.OnActivate() method should get called when the DocViewerViewModel.ShowInMainView<T>() method gets called.
The DocumentViewModel.OnDeactivate() method should get called on the old DocumentViewModel when a new one is being shown by calling the DocViewerViewModel.ShowInMainView<T>()
Our Solution for that Problem is to remove the use Screen as BaseClass for DocViewerViewModel an implement the Conductor Logic our self.

Good or bad practice for Dialogs in wpf with MVVM?

I lately had the problem of creating add and edit dialogs for my wpf app.
All I want to do in my code was something like this. (I mostly use viewmodel first approach with mvvm)
ViewModel which calls a dialog window:
var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
// Do anything with the dialog result
How does it work?
First, I created a dialog service:
public interface IUIWindowDialogService
{
bool? ShowDialog(string title, object datacontext);
}
public class WpfUIWindowDialogService : IUIWindowDialogService
{
public bool? ShowDialog(string title, object datacontext)
{
var win = new WindowDialog();
win.Title = title;
win.DataContext = datacontext;
return win.ShowDialog();
}
}
WindowDialog is a special but simple window. I need it to hold my content:
<Window x:Class="WindowDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="WindowDialog"
WindowStyle="SingleBorderWindow"
WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight">
<ContentPresenter x:Name="DialogPresenter" Content="{Binding .}">
</ContentPresenter>
</Window>
A problem with dialogs in wpf is the dialogresult = true can only be achieved in code. That's why I created an interface for my dialogviewmodel to implement it.
public class RequestCloseDialogEventArgs : EventArgs
{
public bool DialogResult { get; set; }
public RequestCloseDialogEventArgs(bool dialogresult)
{
this.DialogResult = dialogresult;
}
}
public interface IDialogResultVMHelper
{
event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
}
Whenever my ViewModel thinks it's time for dialogresult = true, then raise this event.
public partial class DialogWindow : Window
{
// Note: If the window is closed, it has no DialogResult
private bool _isClosed = false;
public DialogWindow()
{
InitializeComponent();
this.DialogPresenter.DataContextChanged += DialogPresenterDataContextChanged;
this.Closed += DialogWindowClosed;
}
void DialogWindowClosed(object sender, EventArgs e)
{
this._isClosed = true;
}
private void DialogPresenterDataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
var d = e.NewValue as IDialogResultVMHelper;
if (d == null)
return;
d.RequestCloseDialog += new EventHandler<RequestCloseDialogEventArgs>
(DialogResultTrueEvent).MakeWeak(
eh => d.RequestCloseDialog -= eh;);
}
private void DialogResultTrueEvent(object sender,
RequestCloseDialogEventArgs eventargs)
{
// Important: Do not set DialogResult for a closed window
// GC clears windows anyways and with MakeWeak it
// closes out with IDialogResultVMHelper
if(_isClosed) return;
this.DialogResult = eventargs.DialogResult;
}
}
Now at least I have to create a DataTemplate in my resource file(app.xaml or something):
<DataTemplate DataType="{x:Type DialogViewModel:EditOrNewAuswahlItemVM}" >
<DialogView:EditOrNewAuswahlItem/>
</DataTemplate>
Well thats all, I can now call dialogs from my viewmodels:
var result = this.uiDialogService.ShowDialog("Dialogwindow Title", dialogwindowVM);
Now my question, do you see any problems with this solution?
Edit: for completeness. The ViewModel should implement IDialogResultVMHelper and then it can raise it within a OkCommand or something like this:
public class MyViewmodel : IDialogResultVMHelper
{
private readonly Lazy<DelegateCommand> _okCommand;
public MyViewmodel()
{
this._okCommand = new Lazy<DelegateCommand>(() =>
new DelegateCommand(() =>
InvokeRequestCloseDialog(
new RequestCloseDialogEventArgs(true)), () =>
YourConditionsGoesHere = true));
}
public ICommand OkCommand
{
get { return this._okCommand.Value; }
}
public event EventHandler<RequestCloseDialogEventArgs> RequestCloseDialog;
private void InvokeRequestCloseDialog(RequestCloseDialogEventArgs e)
{
var handler = RequestCloseDialog;
if (handler != null)
handler(this, e);
}
}
EDIT 2: I used the code from here to make my EventHandler register weak:
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx
(Website no longer exists, WebArchive Mirror)
public delegate void UnregisterCallback<TE>(EventHandler<TE> eventHandler)
where TE : EventArgs;
public interface IWeakEventHandler<TE>
where TE : EventArgs
{
EventHandler<TE> Handler { get; }
}
public class WeakEventHandler<T, TE> : IWeakEventHandler<TE>
where T : class
where TE : EventArgs
{
private delegate void OpenEventHandler(T #this, object sender, TE e);
private readonly WeakReference mTargetRef;
private readonly OpenEventHandler mOpenHandler;
private readonly EventHandler<TE> mHandler;
private UnregisterCallback<TE> mUnregister;
public WeakEventHandler(EventHandler<TE> eventHandler,
UnregisterCallback<TE> unregister)
{
mTargetRef = new WeakReference(eventHandler.Target);
mOpenHandler = (OpenEventHandler)Delegate.CreateDelegate(
typeof(OpenEventHandler),null, eventHandler.Method);
mHandler = Invoke;
mUnregister = unregister;
}
public void Invoke(object sender, TE e)
{
T target = (T)mTargetRef.Target;
if (target != null)
mOpenHandler.Invoke(target, sender, e);
else if (mUnregister != null)
{
mUnregister(mHandler);
mUnregister = null;
}
}
public EventHandler<TE> Handler
{
get { return mHandler; }
}
public static implicit operator EventHandler<TE>(WeakEventHandler<T, TE> weh)
{
return weh.mHandler;
}
}
public static class EventHandlerUtils
{
public static EventHandler<TE> MakeWeak<TE>(this EventHandler<TE> eventHandler,
UnregisterCallback<TE> unregister)
where TE : EventArgs
{
if (eventHandler == null)
throw new ArgumentNullException("eventHandler");
if (eventHandler.Method.IsStatic || eventHandler.Target == null)
throw new ArgumentException("Only instance methods are supported.",
"eventHandler");
var wehType = typeof(WeakEventHandler<,>).MakeGenericType(
eventHandler.Method.DeclaringType, typeof(TE));
var wehConstructor = wehType.GetConstructor(new Type[]
{
typeof(EventHandler<TE>), typeof(UnregisterCallback<TE>)
});
IWeakEventHandler<TE> weh = (IWeakEventHandler<TE>)wehConstructor.Invoke(
new object[] { eventHandler, unregister });
return weh.Handler;
}
}
This is a good approach and I used similar ones in the past. Go for it!
One minor thing I'd definitely do is make the event receive a boolean for when you need to set "false" in the DialogResult.
event EventHandler<RequestCloseEventArgs> RequestCloseDialog;
and the EventArgs class:
public class RequestCloseEventArgs : EventArgs
{
public RequestCloseEventArgs(bool dialogResult)
{
this.DialogResult = dialogResult;
}
public bool DialogResult { get; private set; }
}
I've been using an almost identical approach for several months now, and I'm very happy with it (i.e. I haven't yet felt the urge to rewrite it completely...)
In my implementation, I use a IDialogViewModel that exposes things such as the title, the standad buttons to show (in order to have a consistent apparence across all dialogs), a RequestClose event, and a few other things to be able to control the window size and behavior
If you are talking about dialogue windows and not just about the pop-up message boxes, please consider my approach below. The key points are:
I pass a reference to Module Controller into the constructor of each ViewModel (you can use injection).
That Module Controller has public/internal methods for creating dialogue windows (just creating, without returning a result). Hence to open a dialogue window in ViewModel I write: controller.OpenDialogEntity(bla, bla...)
Each dialogue window notifies about its result (like OK, Save, Cancel, etc.) via Weak Events. If you use PRISM, then it's easier to publish notifications using this EventAggregator.
To handle dialogue results, I'm using subscription to notifications (again Weak Events and EventAggregator in case of PRISM). To reduce dependency on such notifications, use independent classes with standard notifications.
Pros:
Less code. I don't mind using interfaces, but I've seen too many projects where excessiveness of using interfaces and abstraction layers cause more trouble than help.
Open dialogue windows through Module Controller is a simple way to avoid strong references and still allows to use mock-ups for testing.
Notification through weak events reduce number of potential memory leaks.
Cons:
Not easy to distinguish required notification from others in the handler. Two solutions:
send a unique token on opening a dialogue window and check that token in the subscription
use generic notification classes <T> where T is enumeration of entities (or for simplicity it can be type of ViewModel).
For a project should be an agreement about using notification classes to prevent duplicating them.
For enormously large projects the Module Controller can be overwhelmed by methods for creating windows. In this case it's better to split it up in several modules.
P.S. I have been using this approach for quite a long time now and ready to defend its eligibility in comments and provide some examples if required.

Categories

Resources