4

我已经使用 Caliburn.Micro 和 Modern UI ( https://mui.codeplex.com ) 开始了一个项目,并且在让 IContent 的导航事件在我的视图模型上触发时遇到了一些困难。我已经将两者联系起来,可以通过以下方式相互合作:

CM 引导程序:

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);
    }
}

现代 UI 内容加载器:

[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 (外壳)

<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 {

}

每个子视图都被导出并实现 IContent,如下所示:

[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
}

但这些都没有开火。经过一些调试后,我发现ModernFrame正在检查(SettingsView as IContent)事件,因为它只是一个普通的UserControl. 因此,我创建了一个自定义 UserControl 类,试图将事件传递给 ViewModel:

多内容控件

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);
    }
}

然后我修改了视图以使用 Message.Attach 监听事件:

设置视图

<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>

唯一不会触发的事件是 NavigatedTo 所以我相信 Message.Attach 直到事件被调度之后才会被应用。我可能这样做是一种非常错误的方式,并且对大规模重建持开放态度。

4

2 回答 2

9

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)

于 2013-06-27T09:24:50.140 回答
4

我对我的 IContent 视图执行了以下操作,并在我的 ViewModel 上实现了 IContent。

   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);
            }

        }
    }
于 2013-11-07T16:05:36.510 回答