65.9K
CodeProject 正在变化。 阅读更多。
Home

Blendability - 为 Prism 6 中的区域添加设计时支持

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015 年 11 月 30 日

CPOL

8分钟阅读

viewsIcon

15397

downloadIcon

189

为 Prism 6 中的区域添加设计时支持

引言

当您使用 Prism 和区域开发基于 XAML 的复合应用程序时,在设计时处理 Shell 或充当 Shell/仪表板的区域时,您可能会遇到一个非常令人沮丧的问题。

也就是说,这样

正如您所看到的,我们已经定义了区域,但我们并没有真正好的方法来了解 Prism 在运行时将内容加载到这些区域后,应用程序的外观是什么样的。

在本文中,我将向您展示如何将视图及其关联的设计时视图模型加载到这些区域,以便我们得到这样的结果

只对如何使用它感兴趣,而不是如何构建它?跳转到底部

背景

该实现的想法最初源于这篇文章,该文章写于 2011 年,用于 Prism 4 和 MEF。从那时起,关于这个主题的文档并不多。Prism 已发展到版本 6,虽然 MEF 在进行 DI 方面仍然很棒,但还有其他 DI 容器可以提供同样好甚至更好的效果。我还要在我的待办事项清单上加上几点,我想确保它们得到满足

  1. 我不想为了获得设计时支持而必须(再次)引导整个应用程序——实际、预期的运行时对象的视图、视图模型及其相关依赖项的对象图在添加设计时支持之前可能已经很长了——我的解决方案不应该强迫开发人员添加大量被认为不必要或复杂的代码。
     
  2. 这是我希望开发人员明确且知情地订阅的行为——开启或关闭它不应该需要去一个不直观的地方进行切换、配置或选择加入。另外,我不想让这个解决方案感觉与 Prism 的现有方向明显偏离。我们应该始终能够在不花费半小时撤销的情况下回退到 Prism 的默认功能。
     
  3. 此外,我可能还没有在模块级别完全构建好设计时支持,因此我想选择将哪些视图加载到其 respective 区域。如果我明确订阅了,但某个视图没有在设计时加载到某个区域,那么很清楚是为什么。
     
  4. MEF 很棒,但现在是 2015 年,还有其他 DI 容器同样出色,甚至更好。这个解决方案应该可以轻松地转换为其他 DI 容器。
     
  5. 单个视图已经有了设计时视图模型——当看到视图加载到区域中时,我想利用这一点,以便我在单个视图级别看到的设计时数据与我加载视图到区域时看到的设计时数据相同。
     
  6. 这一点不言而喻,但它应该能与 Prism 6 良好配合

Prism 为什么不提供此功能?

这是一个很好的问题!从哲学的角度来说,我没有答案。管理 Prism 库的团队可能比我更能回答这个问题。但从技术角度来看,实际上有很多原因。

首先,如果您深入研究 Prism 区域管理器的源代码,您会发现以下用于 RegionName 附加属性的代码。

public static readonly DependencyProperty RegionNameProperty = DependencyProperty.RegisterAttached("RegionName", typeof(string), typeof(RegionManager), new PropertyMetadata(OnSetRegionNameCallback));

private static void OnSetRegionNameCallback(DependencyObject element, DependencyPropertyChangedEventArgs args)
{
  if (!IsInDesignMode(element))
  {
    CreateRegion(element);
  }
}

所以,一开始,如果我们正在设计时操作,什么都不会发生。

此外,如果我们查看 CreateRegion 的实现

private static void CreateRegion(DependencyObject element)
{
    IServiceLocator locator = ServiceLocator.Current;
    DelayedRegionCreationBehavior regionCreationBehavior = locator.GetInstance<DelayedRegionCreationBehavior>();
    regionCreationBehavior.TargetElement = element;
    regionCreationBehavior.Attach();
}

我们看到我们获得了一个 DelayedRegionCreationBehavior,它会等待目标元素引发 Loaded 事件以实际注入视图。运行时这不是问题;然而,在设计时,Loaded 事件直到用户控件被显式放置在窗口上后才会调用——根据我的经验,即使那样,它引发的几率也有些不可靠。还有一整系列需要考虑的因素,但 Loaded 事件是我们必须克服的障碍。

假设我们已经克服了上述限制——并且我们能够在设计时引发事件——我们仍然需要一个视图(及其关联的视图模型)被注册到区域——毕竟,区域管理器必须注入一些东西……对吗?

让我们写一些代码!

好的,所以让我们从解决最后一项问题开始——区域管理器必须注入一些东西。

让我们开始构建一个类,它将仅在设计时提供视图。我们称之为 DesignTimeViewProviderBase

public abstract class DesignTimeViewProviderBase
{
    private readonly IDictionary<string, Type> ViewRegistrations = new Dictionary<string, Type>();

    private bool IsInitialized = false;

    public void Initialize()
    {
        if(IsInitialized)
        { return; }

        RegisterViewsWithContainer();
        RegisterViewsWithRegions();
        IsInitialized = true;
    }

    protected abstract void RegisterViewsWithContainer();

    protected abstract void RegisterViewsWithRegions();

    protected abstract object ResolveView(Type viewType);

    protected void RegisterViewWithRegion<T>(string regionName)
    {
        if(ViewRegistrations.ContainsKey(regionName) == false)
        {
            ViewRegistrations.Add(regionName, typeof(T));
        }
    }

    public object GetViewForRegion(string regionName)
    {
        object view = null;
        Type viewType;

        if(ViewRegistrations.TryGetValue(regionName, out viewType))
        {
            try
            {
                view = ResolveView(viewType);
            }catch(Exception ex)
            {
                view = new TextBlock() { Text = ex.Message };
            }
        }
        else
        {
            view = new TextBlock() { Text = "No view registration for region " + regionName };
        }

        return view;
    }
}

好的,一开始,您可以看到我们并不追求复杂——这个基类包含视图类型与区域名称的一对一映射(因为在设计时我们每个区域名称只能看到一个视图),并提供抽象方法,其中任何特定于容器的代码都应该放在这里。如果未能获取视图,我们将向开发人员显示一个文本块,详细说明问题所在。

但是,DesignTimeViewProviderBase 是一个抽象类,所以我们还没做完。因为我是 Unity 的粉丝,所以让我们在此基础上添加一个类来与 Unity 一起使用。

public abstract class UnityDesignTimeViewProvider : DesignTimeViewProviderBase
{
    protected readonly IUnityContainer container = new UnityContainer();

    protected override object ResolveView(Type viewType)
    {
        return container.Resolve(viewType);
    }
}

正如您所看到的,这现在提供了一个容器和一个特定于 Unity 的解析视图的方法,同时仍然将 RegisterViewsWithContainerRegisterViewsWithRegions 留给实际的具体实现——此外,这提供了一个很好的点,其他 DI 容器可以根据需要介入。

既然我们已经有了一个可以作为视图提供者的基类,让我们来处理如何使用它。

我们将利用依赖属性工作方式中的一个小怪癖。如果您读者有更好的解释它为何如此工作的链接,我将不胜感激。

您的附加依赖属性的命名很重要——如果您有多个相互依赖的附加依赖属性来提供值(就像我们一样),并且您没有设置某种值强制转换(正如我所做的那样),那么 setter 将按字母顺序执行。这对我们很重要,因为我们需要我们的 setter 在 Prism 的 RegionName 设置后执行。所以在我们的例子中,一个名为 DesignTimeViewProvider 的附加属性是不行的,因为它会过早执行;但是一个名为 ViewProvider 的附加属性是可以的,因为它在 RegionName 之后执行。

现在,当 ViewProvider 附加属性被设置时,我们将获取 RegionName 附加属性的值,提取与该区域关联的设计时视图,然后将其注入。

让我们首先创建一个名为 RegionProvider 的类,它允许我们设置设计时视图提供者。

public class RegionProvider
{
    public static DesignTimeViewProviderBase GetViewProvider(DependencyObject obj)
    {
        return (DesignTimeViewProviderBase)obj.GetValue(ViewProviderProperty);
    }

    public static void SetViewProvider(DependencyObject obj, DesignTimeViewProviderBase value)
    {
        obj.SetValue(ViewProviderProperty, value);
    }

    public static readonly DependencyProperty ViewProviderProperty =
        DependencyProperty.RegisterAttached("ViewProvider", typeof(DesignTimeViewProviderBase), typeof(RegionProvider), new PropertyMetadata(null, ViewProvider_Changed));

到目前为止,对于一个附加依赖属性来说,这是相当标准的;让我们看看 ViewProvider_Changed 回调。

private static void ViewProvider_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    if (DesignerProperties.GetIsInDesignMode(sender) == false)
    { return; } //we're in run-time, so the rest is not necessary  

    var viewProvider = e.NewValue as DesignTimeViewProviderBase;
    var regionName = RegionManager.GetRegionName(sender);

    object designTimeView = null;
    try
    {
        viewProvider.Initialize();
        designTimeView = viewProvider.GetViewForRegion(regionName);
    }
    catch (Exception ex)
    {
        designTimeView = new TextBlock() { Text = ex.Message };
    }

    //Prism currently supports ContentControl, Selector, and ItemsControl.
    if (sender is ContentControl)
    {
        (sender as ContentControl).Content = designTimeView;
    }
    else if (sender is Selector)
    {
        var itemsSource = new object[] { designTimeView };
        (sender as Selector).ItemsSource = itemsSource;
        (sender as Selector).SelectedItem = itemsSource.First();
    }
    else if (sender is ItemsControl)
    {
        (sender as ItemsControl).ItemsSource = new object[] { designTimeView };
    }
}

所以我们要做的第一件事是退出,如果我们不在设计模式下——显然,这是我们不想在运行时执行的代码。

接下来,由于 RegionName 是同一依赖对象上的附加属性(并且我们是在其设置后执行的),我们可以获取区域的名称,并要求我们新设置的视图提供者获取与该区域关联的设计时视图。一旦我们有了视图,唯一要做的就是根据 Prism 支持的三种容器类型来设置它。

在模块级别设置设计时支持

我想退一步,为现有视图设置一些设计时支持。让我们看看“视图 A”。它非常简单——它只是一个带有绑定文本块的网格。

<Grid>
    <TextBlock Text="{Binding DisplayText}" />
</Grid>

我们将向用户控件添加以下属性。

d:DataContext="{d:DesignInstance Type={x:Type designTime:ViewModel}, IsDesignTimeCreatable=True}"

现在,当我们添加这个类时

namespace ModuleA.Demonstration.DesignTime
{
    public class ViewModel : IViewModel
    {
        /// <summary>
        /// Gets the text to display at design time
        /// </summary>
        public string DisplayText
        {
            get
            { 
                return "Hello Module A from design time view model";
            }
        }
    }
}

我们得到以下结果

好的,这太棒了,我们现在在模块级别有了设计时支持。现在,让我们将其应用到 Shell。

创建设计时视图提供者

现在我们将使用我们之前创建的 UnityDesignTimeViewProvider。除了我们之前创建的 View A 之外,我的示例还包含 View B 和 View C。我们也将设置它们。

public class ViewProvider : UnityDesignTimeViewProvider
{
    protected override void RegisterViewsWithContainer()
    {
        //Region A's view and its dependencies
        container.RegisterType<ModuleA.Demonstration.View>();
        container.RegisterType<ModuleA.Demonstration.IViewModel, ModuleA.Demonstration.DesignTime.ViewModel>();

        //Region B's view and its dependencies
        container.RegisterType<ModuleB.Demonstration.View>();
        container.RegisterType<ModuleB.Demonstration.IViewModel, ModuleB.Demonstration.DesignTime.ViewModel>();

        //Region C's view and its dependencies
        container.RegisterType<ModuleC.Demonstration.View>();
        container.RegisterType<ModuleC.Demonstration.IViewModel, ModuleC.Demonstration.DesignTime.ViewModel>();
    }

    protected override void RegisterViewsWithRegions()
    {
        RegisterViewWithRegion<ModuleA.Demonstration.View>(NamedRegions.ModuleA);
        RegisterViewWithRegion<ModuleB.Demonstration.View>(NamedRegions.ModuleB);
        RegisterViewWithRegion<ModuleC.Demonstration.View>(NamedRegions.ModuleC);
    }
}

如果您仔细查看代码,您会发现每个视图的 DataContext 都是构造函数注入的依赖项;这允许我们在设计时进行此注册,并在引导程序在运行时运行时在关联的 IModule 中注册我们的运行时实现。

整合所有内容

既然一切都已创建,我们终于可以为 Shell 添加设计时支持了。

让我们从将我们刚刚创建的设计时视图提供者添加为 Shell 窗口的资源开始。

<Window.Resources>
        <designTime:ViewProvider x:Key="DesignTimeViewProvider" />
</Window.Resources>

现在,任何地方有内容通过区域加载的地方;我只需要添加 ViewProvider 附加属性,并将其指向此资源,即可获得所需的设计时视图。

本质上,这个

 <GroupBox Grid.Column="0" Header="Module A" prism:RegionManager.RegionName="{x:Static infrastructure:NamedRegions.ModuleA}" />

变成这样

<GroupBox Grid.Column="0" Header="Module A" prism:RegionManager.RegionName="{x:Static infrastructure:NamedRegions.ModuleA}" regions:RegionProvider.ViewProvider="{StaticResource DesignTimeViewProvider}" />

构建解决方案,然后关闭并重新打开 xaml 文件,设计时视图应该就在那里了。

历史

2015-11-30:首次发布

© . All rights reserved.