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)