Caliburn.Micro + MEF + Modern UI: IContent события
Я начал проект с использованием Caliburn.Micro и Modern UI ( https://mui.codeplex.com/), и у меня возникли некоторые трудности с получением событий навигации IContent для моей модели представления. Я уже подключил эти два к работе друг с другом со следующим:
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);
}
}
Современный 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 {
}
Каждое из дочерних представлений экспортируется и реализует 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:
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);
}
}
Затем я изменил представления для прослушивания событий с помощью 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>
Единственное событие, которое не запускается, - это NavigatedTo, поэтому я считаю, что Message.Attach не применяется до тех пор, пока событие не будет отправлено. Я, вероятно, делаю это очень неправильно и я открыт для масштабной реконструкции.
2 ответа
Хорошо, в конце концов, это было не так уж и плохо - конечно, это немного облегчает жизнь, пытаясь донести события до виртуальной машины.
Я создал проводник для ModernFrame
контроль, который существует в ModernWindow
шаблон управления
Вам нужно создать экземпляр проводника в OnViewLoaded
событие виртуальной машины для вашего ModernWindow
поскольку это, кажется, лучшее место (то есть навигация еще не произошла, но элемент управления полностью загружен и определил свой шаблон)
// 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);
}
}
Код проводника выглядит следующим образом:
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
}
Теперь любой вид, который вы добавляете IContent
Интерфейс будет автоматически вызывать свои методы, вызываемые фреймом, когда происходит навигация
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
}
}
Я проверил, и это работает со всеми 4 событиями навигации, которые появляются на IContent
- так как он проходит через EventArgs
Вы можете отменить событие навигации непосредственно с ВМ или сделать то, что вы обычно делаете в сценарии только для просмотра
Я думаю, что это, вероятно, самый безболезненный способ, которым я мог придумать - буквально одна строка кода в окне и реализовать интерфейс на ВМ, и все готово:)
Редактировать:
Единственное, что я, вероятно, добавлю, это выдача исключений или, возможно, отладка уведомлений журнала при добавлении проводника в окно на случай, если он по какой-то причине не сможет найти фрейм (возможно, имя фрейма может измениться позже. выпуск м: ии)
Я сделал следующее в своих IContent Views и внедрил IContent в мои 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);
}
}
}