I've started a project using Caliburn.Micro and Modern UI (https://mui.codeplex.com) and am having some difficulty getting the navigation events of IContent to fire on my view model. I've already got the two hooked up to work with each other with the following:
CM Bootstrapper:
public class CMBootstrapper : Bootstrapper<IShell> {
private CompositionContainer container;
private DirectoryCatalog catalog;
public CMBootstrapper() { }
protected override void Configure() {
catalog = new DirectoryCatalog(".", "*.*");
container = new CompositionContainer(catalog);
var compositionBatch = new CompositionBatch();
compositionBatch.AddExportedValue<IWindowManager>(new WindowManager());
compositionBatch.AddExportedValue<IEventAggregator>(new EventAggregator());
compositionBatch.AddExportedValue(container);
container.Compose(compositionBatch);
}
protected override IEnumerable<Assembly> SelectAssemblies() {
List<Assembly> assemblies = new List<Assembly>();
assemblies.Add(Assembly.GetExecutingAssembly());
return assemblies;
}
protected override object GetInstance(Type serviceType, string key) {
string contract = string.IsNullOrEmpty(key) ? AttributedModelServices.GetContractName(serviceType) : key;
var exports = container.GetExportedValues<object>(contract);
if (exports.Count() > 0)
return exports.First();
throw new Exception(string.Format("Could not locate any instances of contract {0}.", contract));
}
protected override IEnumerable<object> GetAllInstances(Type serviceType) {
return container.GetExportedValues<object>(AttributedModelServices.GetContractName(serviceType));
}
protected override void BuildUp(object instance) {
container.SatisfyImportsOnce(instance);
}
}
Modern UI Content Loader:
[Export]
public class MuiContentLoader : DefaultContentLoader {
protected override object LoadContent(Uri uri) {
var content = base.LoadContent(uri);
if (content == null)
return null;
// Locate VM
var viewModel = ViewModelLocator.LocateForView(content);
if (viewModel == null)
return content;
// Bind VM
if (content is DependencyObject)
ViewModelBinder.Bind(viewModel, content as DependencyObject, null);
return content;
}
}
MuiView.xaml (Shell)
<mui:ModernWindow x:Class="XMOperations.Views.MuiView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mui="http://firstfloorsoftware.com/ModernUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
ContentLoader="{StaticResource ModernContentLoader}"
d:DesignHeight="300" d:DesignWidth="300">
<mui:ModernWindow.TitleLinks>
<mui:Link DisplayName="Settings" Source="/Views/SettingsView.xaml" />
</mui:ModernWindow.TitleLinks>
<mui:ModernWindow.MenuLinkGroups>
<mui:LinkGroupCollection>
<mui:LinkGroup GroupName="Hello" DisplayName="Hello">
<mui:LinkGroup.Links>
<mui:Link Source="/Views/ChildView.xaml" DisplayName="Click me"></mui:Link>
</mui:LinkGroup.Links>
</mui:LinkGroup>
</mui:LinkGroupCollection>
</mui:ModernWindow.MenuLinkGroups>
MuiViewModel
[Export(typeof(IShell))]
public class MuiViewModel : Conductor<IScreen>.Collection.OneActive, IShell {
}
Each of the child views are exported and implement IContent like so:
[Export]
[PartCreationPolicy(CreationPolicy.Shared)]
public class SettingsViewModel : Screen, IContent {
#region IContent Implementation
public void OnFragmentNavigation(FragmentNavigationEventArgs e) {
Console.WriteLine("SettingsViewModel.OnFragmentNavigation");
}
public void OnNavigatedFrom(NavigationEventArgs e) {
Console.WriteLine("SettingsViewModel.OnNavigatedFrom");
}
public void OnNavigatedTo(NavigationEventArgs e) {
Console.WriteLine("SettingsViewModel.OnNavigatedTo");
}
public void OnNavigatingFrom(NavigatingCancelEventArgs e) {
Console.WriteLine("SettingsViewModel.OnNavigatingFrom");
}
#endregion
}
But none of those were firing. After some debugging I found that ModernFrame
was checking (SettingsView as IContent)
for the events, which wouldn't have them because it was just a plain UserControl
. So I created a custom UserControl class in an attempt to pass the events along to the ViewModel:
MuiContentControl
public delegate void FragmentNavigationEventHandler(object sender, FragmentNavigationEventArgs e);
public delegate void NavigatedFromEventHandler(object sender, NavigationEventArgs e);
public delegate void NavigatedToEventHandler(object sender, NavigationEventArgs e);
public delegate void NavigatingFromEventHandler(object sender, NavigatingCancelEventArgs e);
public class MuiContentControl : UserControl, IContent {
public event FragmentNavigationEventHandler FragmentNavigation;
public event NavigatedFromEventHandler NavigatedFrom;
public event NavigatedToEventHandler NavigatedTo;
public event NavigatingFromEventHandler NavigatingFrom;
public MuiContentControl() : base() {
}
public void OnFragmentNavigation(FragmentNavigationEventArgs e) {
if(FragmentNavigation != null)
FragmentNavigation(this, e);
}
public void OnNavigatedFrom(NavigationEventArgs e) {
if (NavigatedFrom != null)
NavigatedFrom(this, e);
}
public void OnNavigatedTo(NavigationEventArgs e) {
if(NavigatedTo != null)
NavigatedTo(this, e);
}
public void OnNavigatingFrom(NavigatingCancelEventArgs e) {
if(NavigatingFrom != null)
NavigatingFrom(this, e);
}
}
Then I modified the views to listen for the events with Message.Attach:
SettingsView
<local:MuiContentControl x:Class="XMOperations.Views.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mui="http://firstfloorsoftware.com/ModernUI"
xmlns:cal="http://www.caliburnproject.org"
xmlns:local="clr-namespace:XMOperations"
cal:Message.Attach="[Event FragmentNavigation] = [Action OnFragmentNavigation($source, $eventArgs)];
[Event NavigatedFrom] = [Action OnNavigatedFrom($source, $eventArgs)];
[Event NavigatedTo] = [Action OnNavigatedTo($source, $eventArgs)];
[Event NavigatingFrom] = [Action OnNavigatingFrom($source, $eventArgs)]"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Style="{StaticResource ContentRoot}">
<mui:ModernTab SelectedSource="/Views/Settings/AppearanceView.xaml" Layout="List" ContentLoader="{StaticResource ModernContentLoader}">
<mui:ModernTab.Links>
<mui:Link DisplayName="Appearance" Source="/Views/Settings/AppearanceView.xaml" />
</mui:ModernTab.Links>
</mui:ModernTab>
</Grid>
The only event that doesn't fire is NavigatedTo so I believe that Message.Attach is not being applied until after the event is dispatched. I am probably doing this a very wrong way and am open to massive reconstruction.
Ok this wasn't so bad in the end - it certainly makes life a bit easier in trying to get the events to be passed across to the VM
I created a conductor for the ModernFrame
control that exists in the ModernWindow
controls template
You need to create an instance of the conductor in the OnViewLoaded
event of the VM for your ModernWindow
as this seems to be the best place (i.e. no navigation has happened yet but the control has fully loaded and has resolved it's template)
// Example viewmodel:
public class ModernWindowViewModel : Conductor<IScreen>.Collection.OneActive
{
protected override void OnViewLoaded(object view)
{
base.OnViewLoaded(view);
// Instantiate a new navigation conductor for this window
new FrameNavigationConductor(this);
}
}
The conductor code is as follows:
public class FrameNavigationConductor
{
#region Properties
// Keep a ref to the frame
private readonly ModernFrame _frame;
// Keep this to handle NavigatingFrom and NavigatedFrom events as this functionality
// is usually wrapped in the frame control and it doesn't pass the 'old content' in the
// event args
private IContent _navigatingFrom;
#endregion
public FrameNavigationConductor(IViewAware modernWindowViewModel)
{
// Find the frame by looking in the control template of the window
_frame = FindFrame(modernWindowViewModel);
if (_frame != null)
{
// Wire up the events
_frame.FragmentNavigation += frame_FragmentNavigation;
_frame.Navigated += frame_Navigated;
_frame.Navigating += frame_Navigating;
}
}
#region Navigation Events
void frame_Navigating(object sender, NavigatingCancelEventArgs e)
{
var content = GetIContent(_frame.Content);
if (content != null)
{
_navigatingFrom = content;
_navigatingFrom.OnNavigatingFrom(e);
}
else
_navigatingFrom = null;
}
void frame_Navigated(object sender, NavigationEventArgs e)
{
var content = GetIContent(_frame.Content);
if (content != null)
content.OnNavigatedTo(e);
if (_navigatingFrom != null)
_navigatingFrom.OnNavigatedFrom(e);
}
void frame_FragmentNavigation(object sender, FragmentNavigationEventArgs e)
{
var content = GetIContent(_frame.Content);
if (content != null)
content.OnFragmentNavigation(e);
}
#endregion
#region Helpers
ModernFrame FindFrame(IViewAware viewAware)
{
// Get the view for the window
var view = viewAware.GetView() as Control;
if (view != null)
{
// Find the frame by name in the template
var frame = view.Template.FindName("ContentFrame", view) as ModernFrame;
if (frame != null)
{
return frame;
}
}
return null;
}
private IContent GetIContent(object source)
{
// Try to cast the datacontext of the attached viewmodel to IContent
var fe = (source as FrameworkElement);
if (fe != null)
{
var content = fe.DataContext as IContent;
if (content != null)
return content;
}
return null;
}
#endregion
}
Now any view which you add the IContent
interface to will automatically get its methods called by the frame whenever navigation occurs
public class TestViewModel : Conductor<IScreen>, IContent
{
public void OnFragmentNavigation(FragmentNavigationEventArgs e)
{
// Do stuff
}
public void OnNavigatedFrom(NavigationEventArgs e)
{
// Do stuff
}
public void OnNavigatedTo(NavigationEventArgs e)
{
// Do stuff
}
public void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
// Do stuff
}
}
I've tested and this works with all 4 navigation events that appear on IContent
- since it passes through the EventArgs
you can cancel the navigation event directly from the VM or do whatever you would normally do in a view only scenario
I think this is probably the most pain-free way I could come up with - literally one line of code in the window and implement the interface on the VM and you are done :)
Edit:
The only thing I'd probably add is some exception throwing or maybe debug log notification when adding the conductor to a window in case it, for some reason, couldn't find the frame (maybe the name of the frame could change in a later release of m:ui)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With