I have a WinRT app which is up and running nicely using the caliburn.micro INavigationService (FrameAdapter). The problem is all of the pages in my app are full pages and they repeat a lot of view xaml markup. What I would like to do would be to have more of a "MasterPages" type of architecture where I have a shell view which defines the main layout and contains a ContentControl and uses the Conductor pattern to switch out the contents of the ContentControl. I have had success with this sort of architecture in the past with WPF and Silverlight so I assume it is possible in WinRT.
The problem is there seems to be a disconnect between the navigation infrastructure (using the FrameAdapter) and the Conductor infrastructure using a ContentControl bound to the ActiveItem of the conductor (like the SimpleNavigation sample).
In the conductor scenario I use ActivateItem:
ActivateItem(new MyViewModel());
but with the INavigationService I use NavigateToViewModel:
navigationService.NavigteToViewModel<MyViewModel>();
and the two don't seem to be connected as far as I can tell.
One idea I had was to create a ConductorNavigationService which implements INavigationService and basically handles the creation and activation of the child screens. While it seems possible it doesn't seem very straight forward so I figured I would check to see if there is an already supported way to do this in caliburn.micro.
Ok my understanding might be a little strained as I've not used WinRT or the INavigationService but I assume Frame is part of RT and INavigationService provides viewmodel resolution and navigation for the frames.
My other assumption is that your frame is a bit like your conductor already, and when you call 'Navigate()' on the frame, it just replaces the content of the frame with the newly specified content. If this is the case then CM is doing view first resolution for viewmodels.
Since you want to go the conductor route, it sounds like you want to ditch the CM implementation of INavigationService and just roll your own to handle the INavigationService navigation methods (e.g. skip the Frames Navigate() method).
A quick look at the CM source reveals that all NavigationService is doing is handling the Navigate events on the frame and then doing VM resolution and setting up the view (something that conductor probably already does). All you would need to do is ensure that your INavigationService implementation just loads the specified view into the shell instead of navigating the frame
You could just need to steal the constructor code for NavigationService and change the implementation of Navigate(), then just call ActivateItem(x) on your shell where x is the instance of the VM. CM will take care of the rest (I think CM boostrapper will already setup your root 'Frame' too so you shouldn't need to worry about that).
e.g.
An implementation may look more like this (bear in mind this is just something I thrown together and may be barefaced lies!):
public class NewFrameAdapter : INavigationService
{
private readonly Frame frame;
private readonly IConductActiveItem shell;
private event NavigatingCancelEventHandler ExternalNavigatingHandler = delegate { };
public NewFrameAdapter(Frame frame)
{
this.frame = frame;
// Might want to tighten this up as it makes assumptions :)
this.shell = (frame as FrameworkElement).DataContext as IConductActiveItem;
}
public bool Navigate(Type pageType)
{
// Do guardclose and deactivate stuff here by looking at shell.ActiveItem
// e.g.
var guard = shell.ActiveItem as IGuardClose;
if (guard != null)
{
var shouldCancel = false;
guard.CanClose(result => { shouldCancel = !result; });
if (shouldCancel)
{
e.Cancel = true;
return;
}
}
// etc
// Obviously since the guard is probably async (assume it is, if not you are ok to continue!) you'd have to not call this code right
// here but I've just stuck it in here as an example
// edit: looking at the code above (the guard code) it looks like this is all sync so the below code should be fine
// You might get away with calling shell.ActivateItem(pageType) as I'm not sure
// if the viewmodel binder in RT would resolve this all for you, but if it doesnt...
// Init the view and then resolve the VM type
ViewLocator.InitializeComponent(pageType);
var viewModel = ViewModelLocator.LocateForView(pageType);
// Activate the VM in the shell)
shell.ActivateItem(viewModel);
}
It shouldn't be too difficult to roll this your own way. Does this help you at all?
Then your XAML would be pretty simple:
<Frame blah blah>
<SomeStaticContent />
<ContentControl x:Name="ActiveItem" /> <!-- The dynamic bit... -->
<SomeMoreStaticContent />
</Frame>
I'm thinking that this will probably be a hybrid of view-first and viewmodel-first since your root Frame will be using view-first, and your conductor will be using ActivateItem() which takes a viewmodel and then resolves the view when the binder kicks in, but if my assumptions are ok, it should work
Related
I'm trying to inject the Dispatcher of my ShellPage into my ViewModel with Unity and Prism. Because the dispatcher is available after the shell is being created, I can't register the Dispatcher in time.
First step:
protected override Task OnInitializeAsync(IActivatedEventArgs args)
{
[...]
Container.RegisterInstance<IResourceLoader>(resourceLoader);
Secondly the CreateShell method will be executed.
protected override UIElement CreateShell(Frame rootFrame)
{
var shell = Container.Resolve<ShellPage>();
Container.RegisterType<MyViewModel>(new InjectionProperty(nameof(MyViewModel.Dispatcher), shell.Dispatcher));
Although I inject the Dispatcher into MyViewModel into the propertie Dispatcher is null. Maybe I need something like recomposing in MEF? How can I achieve the propertie injection into MyViewModel?
I think is not possible.
Recomposing like MEF is not supported.
The easy way is creating a helper to get the Dispatcher of the current view and use it in your ViewModels
Please see this example:
http://mikaelkoskinen.net/post/fixing-coredispatcher-invoke-how-to-invoke-method-in-ui-thread-in-windows-8-release-preview
This example is for Windows 8 but you can use it for UWP apps
just when I use this approach I do small changes for UWP apps:
In Onlaunched event in app.xaml.cs
I add UIDispatcher in a different place.
if (rootFrame.Content == null)
{
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
UIDispatcher.Initialize();
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
// Ensure the current window is active
Window.Current.Activate();
I have a MvxViewController and in the ViewDidLoad i bind the button click to the viewmodel. When the button is clicked I open another view in which I will need to return a string back to my first view
public override void ViewDidLoad ()
{
var set = this.CreateBindingSet<MyView1, MyView1ViewModel>();
set.Bind(myButton).To(vm => vm.MyButtonCommand);
set.Apply();
}
public ICommand MyButtonCommand
{
get
{
_myButtonCommand = _myButtonCommand ?? new MvxCommand(MyButtonCommandClick);
return _myButtonCommand;
}
}
private void MyButtonCommandClick()
{
ShowViewModel<ViewModelNumber2>();
}
After some logic is ran in my second view I want to return the string
private void SomeMethodInViewModelNumber2()
{
//Raise event that will get pickup up in MyView
//Or somehow get "SomeString"
if (OnMyResult != null)
OnMyResult ("SomeString");
}
The problem is that I don't want to send the string back using the messenger. I have my reasons but basically because ViewModelNumber2 can be opened from many different places and works slightly different and managing the different messages that would need to be sent back and where to subscribe to these messages would be a mess
Is there any way that I can do something like the below?
public override void ViewDidLoad ()
{
var set = this.CreateBindingSet<MyView1, MyView1ViewModel>();
set.Bind(myButton).To(vm => vm.MyButtonCommand).OnMyResult((myString) => {Process(myString)});
set.Apply();
}
Or perhaps when I create ViewModelNumber2 I should pass a callBack into the constructor and use that to send the string back from ViewModelNumber2 to MyView1ViewModel
ShowViewModel<ViewModelNumber2>(OnMyResult);
What is the best way to do this?
In short: I don't know what "the best way to do this" is.
The area of ChildViewModel-ParentViewModel messages is complicated - especially because on platforms like Android using Activities and WindowsPhone using Pages you have no guarantee that the ParentViewModel will be in memory when the Child is shown. (Note: this isn't a problem on iOS as its "app suspension" model is simpler)
When I do need one ViewModel returning data to another, then:
Often I try to implement the data collection views as "popup dialogs" rather than as "whole pages" - this makes the parent-child ViewModel relationship more correct - and ensures the parent ViewModel will be in memory when the child closes.
Often I recommend people use a Messenger-based technique like Greg describes in: http://www.gregshackles.com/2012/11/returning-results-from-view-models-in-mvvmcross/
often I've done this messaging via background services rather than via ViewModel-ViewModel messaging (a bit like the way screens are updated in https://github.com/MvvmCross/NPlus1DaysOfMvvmCross/tree/master/N-17-CollectABull-Part6)
Another solution I've used is to:
implement a IDropBoxService singleton - with an API like void Deposit(key, value) and bool TryCollect(key, out value)
allow the closing "child" ViewModels to leave "values" when they close
implement IVisible functionality in my "parent" ViewModel - like in https://github.com/MvvmCross/NPlus1DaysOfMvvmCross/blob/master/N-42-Lifecycles/Lifecycle.Core/ViewModels/FirstViewModel.cs#L10
use the IVisible method to check for messages
To implement anything perfectly, you really should add serialisation code to make sure this all works during "tombstoning" on all platforms... but often this is overkill - for a simple data collection dialog users often don't need "perfect" tombstoning support.
I'd like to understand the best practice for structuring an MVVM solution.
Currently I've created a separate project for my View and my ViewModel. My View project references my ViewModel project. So far so good. When it comes to implementing navigation my ViewModel classes need access to the RootFrame in order to navigate around, but the RootFrame resides in App.xaml which is in the View project. So I have a circular dependency issue here.
Is there a recommended structure I should be using? I could just lump this all into one big project but in order to decouple the View and ViewModel I feel like having separate projects is a better approach.
When it comes to implementing navigation my ViewModel classes need access to the RootFrame
This is a false assumption.
You could use a message broker (a single object) that is responsible for distributing messages between publishers (ViewModels) and subscribers (some object that is responsible for opening Views).
Most MVVM frameworks have such a broker.
About the dependencies
The single responsibility of the Broker is to raise events. So, in general, it exposes a couple of methods that can be called by publishers and a couple of events that can be registered to by subscribers.
In MVVM you could use this mechanism to have a ViewModel raise the event that signals that a View should be opened and a View Manager that subscribes to this event. The View Manager should be able to instantiate a View and attach the correct ViewModel.
To prevent the ViewManager from needing references to all Views and ViewModels you could pass to the event logical names (just a string) and have the View Manager look up the matching View(Model) type by using reflection or a static list in a configuration file.
This way you do NOT need any circular references references. In fact, when you find you need references that go against the proper dependencies in MVVM, you should first doubt the setup and after that consider using a base class for the View and/or ViewModels.
There is no best practice for MVVM since it is a design pattern that everybody implements differently according to their preference. I've seen quite a few different implementations but have never seen a views and view models in separate projects. I would recommend just keeping them in different folders in the same project and put them in different namespaces.
e.g. Your View Models can go in a ViewModels folder and be in the namespace MyProject.ViewModels
Your Views can go in a Views folder and be in the namespace MyProject.Views
And the same for Models if you are using them
Check out Rachel's good answer in this post. I also like to separate my Views and ViewModels, because then I know when I screwed up MVVM ground rules.
Your ViewModel should not have any references to the View, but the View must have a reference to the ViewModel. Consider, for example, my custom SplashScreen factory (the two important lines are "var viewModel..." and "var splashScreen..."):
namespace MyCompany.Factories
{
using System.Threading;
using MyCompany.Views;
using MyCompany.ViewModels;
public static class SplashScreenFactory
{
public static SplashScreenViewModel CreateSplashScreen(
string header, string title, string initialLoadingMessage, int minimumMessageDuration)
{
var viewModel = new SplashScreenViewModel(initialLoadingMessage, minimumMessageDuration)
{
Header = header,
Title = title
};
Thread thread = new Thread(() =>
{
var splashScreen = new SplashScreenView(viewModel);
splashScreen.Topmost = true;
splashScreen.Show();
splashScreen.Closed += (x, y) => splashScreen.Dispatcher.InvokeShutdown();
System.Windows.Threading.Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
return viewModel;
}
}
}
The Factories project has references to both MyCompany.ViewModels and MyCompany.Views. The Views project only has a reference to MyCompany.ViewModels.
We first initiate the ViewModel from our caller (in this case, from SplashScreenFactory; or maybe from App.xaml.cs if you want; I prefer using my own Bootstrapper class for reasons beyond this discussion).
Then we initiate the View by passing the ViewModel reference into the View's constructor. This is called Dependency Injection. You may also need to write a Behaviour for the Window class in order to Close the window from your ViewModel. So now I can do the following from my Bootstrapper class:
/// <summary>
/// Called from this project's App.xaml.cs file, or from the Deals Main Menu
/// </summary>
public class Bootstrapper
{
private SplashScreenViewModel _splashScreenVM;
public Bootstrapper()
{
// Display SplashScreen
_splashScreenVM = SplashScreenFactory.CreateSplashScreen(
"MyCompany Deals", "Planning Grid", "Creating Repositories...", 200);
// Overwrite GlobalInfo parameters and prepare an AuditLog to catch and log errors
ApplicationFactory.AuditedDisplay(
Assembly.GetExecutingAssembly().GetName(),
() =>
{
// Show overwritten version numbers from GlobalInfo on SplashScreen
_splashScreenVM.VersionString = string.Format("v{0}.{1}.{2}",
GlobalInfo.Version_Major, GlobalInfo.Version_Minor, GlobalInfo.Version_Build);
// Initiate ViewModel with new repositories
var viewModel = new PlanningGridViewModel(new MyCompany.Repositories.PlanningGridHeadersRepository(),
new MyCompany.Repositories.PlanningGridLinesRepository(),
_splashScreenVM);
// Initiate View with ViewModel as the DataContext
var view = new PlanningGridView(viewModel);
// Subscribe to View's Activated event
view.Activated += new EventHandler(Window_Activated);
// Display View
view.ShowDialog();
});
}
/// <summary>
/// Closes SplashScreen when the Window's Activated event is raised.
/// </summary>
/// <param name="sender">The Window that has activated.</param>
/// <param name="e">Arguments for the Activated event.</param>
private void Window_Activated(object sender, EventArgs e)
{
_splashScreenVM.ClosingView = true;
}
Note that I have control over the SplashScreen's View by subscribing to the View's Activated event, and then closing the view with the "ClosingView" boolean INotifyProperty that sets the View's "Close" DependencyProperty in turn (sounds complicated, but once you get to know DependencyProperties, it becomes simple).
The point is, don't try to drive your navigation from RootFrame. Just view.Show() the RootFrame, and control the rest from RootFrameViewModel.
Hope this helps :-)
I'm making a small demo application for MVVM with caliburn.
Now I want to show a MessageBox, but the MVVM way.
For dialogs I created an event, that is handled in the ShellView (the root view)
and just calls WindowManager.ShowDialog with a Dialogs ViewModel type.
Seems to stick to MVVM for me.
But what is the way to show a messagebox and get its result (Okay or cancel)?
I already saw this question, but it contains no answer either.
Mr Eisenberg hisself answers with
"Caliburn has services built-in for calling custom message boxes."
Can anyone tell what he means with that? I don't see it in the samples.
As you mentioned, you just prepare the view model (e.g. ConfirmationBoxViewModel) and an appropriate view. You'll have to create two actions (after inheriting the view model from Screen, which is necessary to use TryClose. You can always implement IScreen instead, but that would be more work):
public void OK()
{
TryClose(true);
}
public void Cancel()
{
TryClose(false);
}
and then in your other view model:
var box = new ConfirmationBoxViewModel()
var result = WindowManager.ShowDialog(box);
if(result == true)
{
// OK was clicked
}
Notice that after the dialog closes, you can access the view model properties if you need to pull additional data from the dialog (e.g. Selected item, display name etc).
In the article A Billy Hollis Hybrid Shell (written by the framework coordinator) the author showed a nice way to handle both dialog and message boxes, but he used dependency injection (you can go without DI of course but it makes things simpler). The main idea is that you can let your main window, the one used as the application shell implement an interface that looks something like this:
public interface IDialogManager
{
void ShowDialog(IScreen dialogModel);
void ShowMessageBox(string message, string title = null, MessageBoxOptions options = MessageBoxOptions.Ok, Action<IMessageBox> callback = null);
}
and then he registers this interface with the IoC container, I guess you can use your imagination from there on and if you don't have time then you can look at the source code that accompanies the article.
When the root/main/shell view-model implements a kind of DialogService interface, every other view-model needing to show dialogs will end up with a dependency on the root view-model. Sometimes this might not be desiderable, e.g. if it could cause a dependency loop:
DialogService (aka RootViewModel) -> SomeViewModel -> RootViewModel.
A more involved approach to break this dependency chain (and actually invert it) is the following:
Implement a behavior that detects Window.OnSourceInitialized event and attach it to main view Window component. That is the event fired when the window handle is available. Upon event, behavior will notify some handler passed in via attached property:
<my:WindowSourceBehavior InitListener="{Binding WindowListener}" />
public class WindowSourceBehavior : Behavior<Window>
{
// ...
// boilerplate code for IWindowListener InitListener dependency property
// ...
attachedWindow.SourceInitialized += (sender, evt) =>
{
// ...
InitListener.SourceInitialized(sender as Window);
}
}
DialogService exposes a handler - or interface - as requested by behavior:
public class DialogService : IWindowListener
{
// ...
public void SourceInitialized(Window rootWindow) { /* ... */ }
}
In root view-model, (indirectly) get the DialogService injected as a dependency. During construction, sets view-model bound property, WindowListener, to the DialogService handler/interface:
public MainViewModel(IWindowListener dialogServiceInDisguise)
{
WindowListener = dialogServiceInDisguise;
}
public IWindowListener WindowListener { get; private set; }
Doing so, the DialogService is able to get a hold of root Window, and whichever view-model needs to show a dialog does not create a(n indirect) dependency on main view-model.
In the MvvmCross v3, CustomerManagement example, the method void RequestClose(IMvxViewModel viewModel) closes the top View. How do you close the View of a ViewModel instead?
I wouldn't use that ViewModelCloser method - although it could be extended if you want to.
MvvmCross v3 removed the previous CloseViewModel method - because it didn't really work across all platforms and across all presentation styles - across all of navigation controllers, splitviews, tabs, flyouts, popups, dialogs, etc.
To replace it, v3 introduces a new ViewModel call:
protected bool ChangePresentation(MvxPresentationHint hint)
This is matched in the UIs with an IMvxViewPresenter method:
void ChangePresentation(MvxPresentationHint hint);
To use this, you will need to:
Create a new Hint class - e.g. public class CustomPresentationHint : MvxPresentationHint { /* ... */ }
In each UI project, provide a custom presenter (normally by overriding CreateViewPresenter() in your Setup.cs class) - and in that custom presenter handle the ChangePresentationHint call:
public void ChangePresentation(MvxPresentationHint hint)
{
if (hint is CustomPresentationHint)
{
// your custom actions here
// - which may involve interacting with the RootFrame, with a NavigationController, with the AndroidFragment manager, etc
}
}
In your viewmodel, you can send a CustomPresentationHint when you want to.
I realise this is 'more work' than was required in vNext, but hopefully it's a more flexible, powerful approach.