Caliburn.Micro + MEF + Modern UI: IContent events

独自空忆成欢 提交于 2019-12-04 13:26:18

问题


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.


回答1:


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)




回答2:


I did the follow on my IContent Views and implemented IContent on my ViewModels.

   public void OnFragmentNavigation(FirstFloor.ModernUI.Windows.Navigation.FragmentNavigationEventArgs e)
    {
        if (this.DataContext != null)
        {
            var viewModel = this.DataContext as IContent;
            if (viewModel != null)
            {
                viewModel.OnFragmentNavigation(e);
            }
        }
    }

    public void OnNavigatedFrom(FirstFloor.ModernUI.Windows.Navigation.NavigationEventArgs e)
    {
        if (this.DataContext != null)
        {
            var viewModel = this.DataContext as IContent;
            if (viewModel != null)
            {
                viewModel.OnNavigatedFrom(e);
            }
        }
    }

    public void OnNavigatedTo(FirstFloor.ModernUI.Windows.Navigation.NavigationEventArgs e)
    {
        if (this.DataContext != null)
        {
            var viewModel = this.DataContext as IContent;
            if (viewModel != null)
            {
                viewModel.OnNavigatedTo(e);
            }
        }
    }

    public void OnNavigatingFrom(FirstFloor.ModernUI.Windows.Navigation.NavigatingCancelEventArgs e)
    {
        if (this.DataContext != null)
        {
            var viewModel = this.DataContext as IContent;
            if (viewModel != null)
            {
                viewModel.OnNavigatingFrom(e);
            }

        }
    }


来源:https://stackoverflow.com/questions/16968090/caliburn-micro-mef-modern-ui-icontent-events

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!