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

复合 WPF (CAL, Prism) 简介:第二部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2009年7月19日

CPOL

17分钟阅读

viewsIcon

77035

downloadIcon

1543

一篇展示 CompositeWPF 极其简单实现的文章。

引言

欢迎来到第二部分。如果您是刚看到这篇文章,您可能想先阅读第一部分,在此

在第一部分中,我们探讨了在开始基于 CAL 的应用程序之前可能需要考虑的一些基本知识。我们涵盖了 CAL 的基本功能以及如何构建 CompositeWPF 应用程序的基础。我讨论了模块化、容器如何与 MVP 模式协同工作,以及依赖注入以及何时使用 CAL 应用程序中可用的各种通信方法等主题。

其他主题包括如何在开发环境中组合应用程序以及如何规划协调整体开发工作的某些方面。正如我在那篇文章中多次提到的,这仅仅是 CAL 应用程序结构化各种方法的表面。如果您阅读了复合应用程序指南(一本 305 页的文档),其中很多内容都超出了 CodeProject 文章的范围,而不至于让读者迷失方向。请阅读文档,它真的很值得。

第二部分的内容是什么?

在本文中,我将(字面意思上)构建第一部分的内容。假设您已经玩过第一部分提供的应用程序,看过代码,甚至可能阅读了复合应用程序指南文档,您就会知道有很多可行的路径。我认为这是软件开发的一大奇迹;它就像一个巨大的无定形拼图,我们都知道它在哪里有一个终点,或者一条阻力最小的路径,但我们都在寻找它。就个人而言,我只是一名软件开发人员大约 18 个月,坦白说,我仍然像一只过度兴奋的狗,一直在寻找一个更大的棍子(我肯定会在某个时候用它把自己打晕)!在此期间,我从一个完全的 .NET 新手成长为我雇主的首席技术官,我认为我最好的资产之一是我知道我一无所知。这意味着所有路径都仍然可行,但我能看到当某些事情让生活变得更容易时;在很多方面,CAL 就是其中之一(Sacha 现在可能在瑟瑟发抖,但是;无论如何……我担心我又在 rambling 了……

所以,这是本文将涵盖内容的概要

  • 异步服务调用
  • 主/明细 WPF 数据绑定
  • 用户设置管理
  • 外部样式 DLL
  • 动态皮肤 DLL 发现和加载(CAL 风格!)
  • 动态工具栏
  • CAL 接口

第二部分的新模块

自上一篇文章以来,解决方案已扩展到 21 个项目。本文的新项目包括以下内容

SolutionGraphic.jpg

  • JamSoft.CALDemo.Modules.MusicSearch
  • JamSoft.CALDemo.Modules.MusicSearch.Core
  • JamSoft.CALDemo.Modules.SettingsManager
  • JamSoft.CALDemo.Modules.SettingsManager.Core
  • JamSoft.CALDemo.Modules.SkinManager
  • JamSoft.CALDemo.Modules.SkinManager.Core
  • JamSoft.CALDemo.Modules.ToolBar
  • JamSoft.CALDemo.Modules.ToolBar.Core
  • JamSoft.CALDemo.UI.BlueTheme
  • JamSoft.CALDemo.UI.DefaultTheme
  • JamSoft.WpfThemes.Utils

然而,这些新项目没有自己的解决方案(这是第一部分涵盖的一个主要观点),因此我们只能依赖应用程序目录根目录中的主 JamSoftDebugger.sln 文件。MusicSearch 模块利用 MusicBrainz 网站提供的 Web 服务来搜索唱片艺术家及其在 MusicBrainz 数据库中存储的发行版。我确实考虑过创建一个基于天气预报的模块,但考虑到我是英国人,我想帮助消除我们只谈论奶油茶的观点,所以我决定不这样做。

所以,为了防止更多读者流失,让我们深入研究新代码。

异步服务调用

任何优秀程序员/应用程序的一个标志是使用不会锁定 UI 的方法和过程;锁定 UI 不仅会影响我们的应用程序,还会影响整个机器在我们的任务完成期间的平稳运行……非常糟糕。CodeProject 网站上已经有很多文章比我在这里要详细得多地涵盖了这个问题。因此,我将只介绍应用程序中的一些代码片段,并简要介绍正在发生的事情。我做的第一件事是创建一个类来处理数据检索,该类继承自 AsyncCompletedEventArgs 类。这是 MusicSearch 模块中 ArtistSearchCompletedEventArgs 类中的完整代码。

public class ArtistSearchCompletedEventArgs : AsyncCompletedEventArgs
{
    public List<Artist> ArtistsSearchResults { get; private set; }

    public ArtistSearchCompletedEventArgs(List<Artist> artistsSearchResults)
        : this(artistsSearchResults, null, false, null)
    {
    }

    public ArtistSearchCompletedEventArgs(List<Artist> artistsSearchResults, object userState)
        : this(artistsSearchResults, null, false, userState)
    {
    }

    public ArtistSearchCompletedEventArgs(Exception ex, bool cancelled, object userState)
        : this(null, ex, cancelled, userState)
    {
    }

    public ArtistSearchCompletedEventArgs(List<Artist> artistsSearchResults, 
           Exception ex, bool cancelled, object userState)
        : base(ex, cancelled, userState)
    {
        ArtistsSearchResults = artistsSearchResults;
    }
}

定义好异步“有效载荷”后,您就可以使用 Action<> 委托来构建您的异步方法,如下所示:

public void PerformArtistQueryAsync(Action<object, 
       ArtistSearchCompletedEventArgs> callback, object userState)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
    {
        try
        {
            _eventAggregator.GetEvent<AppStatusMessageEvent>().Publish(
                       "Getting Artists: " + _artistSearchTerm);

            Query<Artist> results = Artist.Query(_artistSearchTerm);

            callback(this, new ArtistSearchCompletedEventArgs(
                               new List<Artist>(results), userState));

        }
        catch (Exception ex)
        {
            callback(this, new ArtistSearchCompletedEventArgs(ex, false, userState));
        }
    }));
}

如您所见,我们正在使用 WaitCallBackThreadPool 来执行工作;这基本上就像使用 BackgroundWorker 一样,因为调用应该是短暂的过程,因此非常适合这种情况。我们做的第一件事是向状态栏发送一条消息,通知用户搜索正在进行中。_artistSearchTerm 是我们 MusicSearchPresenterModel 上双向绑定的公共属性的私有后备字段,该属性绑定到 UI 中的文本框,因此文本框中的任何内容都可以通过此属性在此演示模型的此方法中访问。

因此,为了在应用程序中使用它,我还创建了一个 CAL DelegateCommand<object>。此命令位于 MusicSearchPresenterModel 上,并绑定到 MusicSearchView 中的“搜索艺术家”按钮。该命令具有与之关联的 Executed 和 CanExecute 委托,如下所示:

private void SearchForArtistCommandExecuted(object obj)
{
    _canSearchForArtists = false;
    _searchForArtistCommand.RaiseCanExecuteChanged();

    PerformArtistQueryAsync(ArtistSearchCallback, new object());
}

private bool SearchForArtistCommandCanExecute(object obj)
{
    return _canSearchForArtists;
}

现在,关于回调。PerformArtistQueryAsync 方法调用是我们注册回调方法的地方,当查询完成并且数据准备好在应用程序中处理时,将调用该回调方法。这就是我们使用上面详细介绍的 ArtistsSearchQueryCompletedEventArgs 的地方。

public void ArtistSearchCallback(object sender, ArtistSearchCompletedEventArgs args)
{
    if (args.Error == null)
    {
        MusicSearchObjectConverter objConverter = new MusicSearchObjectConverter();
        Artists = objConverter.Convert(args.ArtistsSearchResults);
    }

    _canSearchForArtists = true;
    _searchForArtistCommand.RaiseCanExecuteChanged();

    _eventAggregator.GetEvent<AppStatusMessageEvent>().Publish("Ready...");
}

这里完成的一项工作是将返回的结果包装起来,因为它们不适合绑定到 UI。在某些方面,这也有好处,因为它将您的应用程序域对象与您无法直接控制的对象分开。如果我们在这里使用 WCF,这将是一个好主意,因为 WCF 对版本非常容忍,而您的 UI/应用程序如果属性/数据开始从您的对象中随机消失,它将无法正常工作。

因此,一旦此服务调用/包装完成,ObservableCollection<BindableArtist> 就被设置为我们演示模型上的 Artists 属性,并且 UI 中的 ListBox 使用从 MusicBrainz Web 服务收到的艺术家集合进行更新。任务完成。显然,在选择艺术家然后单击 UI 中的“搜索发行版”按钮时,完成的过程完全相同,因此我将不再在此重复该过程的解释。所以,让我们来看看 MusicSearchView 中的这个主/明细绑定。

主/明细 WPF 数据绑定

WPF 使使用主/明细数据绑定变得轻而易举。WPF 的数据绑定能力确实是一个很棒的东西,而且在 MusicSearchView 中实际上非常简单。让我们从返回正在使用的数据源开始。它只是我们演示模型上一个简单的 ObservableCollection<>,在 XAML 中公开为名为 Artists 的属性,如下所示:

private ObservableCollection<BindableArtist> _artists;
public ObservableCollection<BindableArtist> Artists
{
    get { return _artists; }
    private set 
    { 
        _artists = value;
        NotifyPropertyChanged("Artists");
    }
}

这在 XAML ListBox 中如下使用:

<ListBox x:Name="lstbArtists" 
 ItemsSource="{Binding Artists}"
 SelectedValue="{Binding SelectedArtist, Mode=TwoWay}"
 DisplayMemberPath="Name"
 Grid.Row="1" 
 Grid.RowSpan="1" 
 Grid.Column="0" 
 Grid.ColumnSpan="1" />

您会注意到 XAML 中的 ListBox.SelectedValue 绑定到演示类上的另一个属性,该属性代表当前选定的 BindableArtist 对象。然后,当单击“搜索发行版”按钮时,它又被用于获取艺术家发行版的方法。

现在,要使用这种特定的主/明细绑定,我们在 UI 中有两个文本框,它们绑定到 ListBox.SelectedItem 属性,以便显示 BindableArtist 对象的内容,如下所示:

<TextBox x:Name="SelectedArtistName" 
     Style="{DynamicResource JamSoftTextBoxStyle}" 
     Text="{Binding Path=SelectedItem.Name, ElementName=lstbArtists, Mode=OneWay}" />
         
<TextBox x:Name="SelectedArtistId" 
     Style="{DynamicResource JamSoftTextBoxStyle}" 
     Text="{Binding Path=SelectedItem.Id, ElementName=lstbArtists, Mode=OneWay}" />

而已,这实际上是此特定场景中实现所需主/明细绑定所需的所有内容。魔法全部由 WPF 处理。文本框元素使用 ElementName 绑定到列表框;然后,我们访问 ListBox 上的 SelectedItem 属性,然后依次访问 BindableArtist 对象 NameId 的属性。完成!

用户设置管理

对于这个应用程序,我创建了一个非常简单的设置管理概念,它不使用 .NET 提供的 Properties.Settings.Default 方法。它是一个非常简单的模块,不适用于生产环境,但它说明了您如何在 CAL 应用程序中处理这个问题,并利用 EventAggregator 保存设置,以及对接口 T GetSettingDefaultValue<T>(string key); 进行简单的通用调用,以从 SettingsManager 模块中检索您的设置。SettingsView.xaml 非常简单,只包含 ComboBox,用于为用户提供选择所需皮肤的方法。视图如下所示:

<UserControl 
    x:Class="JamSoft.CALDemo.Modules.SettingsManager.SettingsView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    >
    <Grid>
        <StackPanel>
            <ComboBox x:Name="cmbSkinPicker" 
                      ItemsSource="{Binding Skins}" 
                      SelectedValue="{Binding Path=CurrentSkin, Mode=TwoWay}" 
                      DisplayMemberPath="Name" />
        </StackPanel>
    </Grid>
</UserControl>

当创建 SettingsManagerPresentationModel 时,它从容器请求的一个内容是对 ISkinManager 的引用,以便在运行时将其作为组合框的 DataContext 提供。这意味着 SettingsView 实际上有两个模型,一个用于常规设置,一个用于皮肤管理器。此接口如下所示:

public interface ISettingsView
{
    ISettingsManagerPresentationModel Model { set; }
    ISkinManager SkinPickerModel { set; }
}

在设置管理器核心库中,我们还有一个定义的 EventAggregator 事件以及一个可用于将设置持久化到序列化设置文件中的自定义对象。此自定义对象非常简单,只包含一个名称和一个对象属性,用于为持久化提供新的设置值。

public class SettingChangedEventArgs : EventArgs
{
    public string SettingName { get; private set; }
    public object SettingValue { get; private set; }

    public SettingChangedEventArgs(string name, object value)
    {
        SettingName = name;
        SettingValue = value;
    }
}

我将不再详细介绍此模块,因为我已说过它不适用于生产代码,但它确实通过集中共享的构造函数和将通用任务“漏斗化”到单个位置来说明了如何在 CAL 应用程序中执行一些管理任务。SettingsManager 类是提供逻辑的那个,它反过来使用 PropertiesCollection 对象,该对象提供了将 NameValueCollection 序列化到自定义 XML 设置文件的能力。

外部样式 DLL

正如多次所说,WPF 通过使用和应用 Style 声明,在逻辑和 UI 的“外观”之间提供了很好的分离。将这些 ResourceDictionary 文件删除到一个完全独立的 DLL 文件中也非常简单,这些文件随后可以被您构建的各种应用程序共享。如果您将这些资源添加到主可执行项目中,您将在应用程序项目中拥有 MyStyle.xaml 文件,然后在您的 App.xaml 中会有类似这样的内容:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="ButtonStyles.xaml"/>
            <ResourceDictionary Source="ListBoxStyles.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

如果将所有这些资源字典移到一个单独的项目中,则需要增强该 XAML 并将其更改为类似以下内容:

<Application.Resources>
    <ResourceDictionary Source="/MyDLLName;component/MyResources.xaml" />
</Application.Resources>

主要区别在于使用 /MyDLLName;component/ 构造来告知运行时您正在寻找的文件位于路径中指定的 DLL 中。但是,在此应用程序中,我们更进一步。样式 DLL 的发现方式与 CAL 使用 DirectoryLookupModuleEnumerator 发现模块的方式相同。这很好地引出了下一节,该节探讨了这一想法。

动态皮肤 DLL 发现和加载

在此应用程序版本中,我们有一个名为 SkinManager 的新模块。当该模块在应用程序启动时初始化时,它会枚举“Skins”目录中的所有 *.dll 文件,并将用 AssemblySkinNameAttributeAssemblySkinDescriptionAttribute 装饰的 DLL 存储在 List<Skin> 中。这两个属性定义在解决方案中包含的 JamSoft.Wpf.Themes.Utils DLL 中。这些是非常简单的自定义属性类,如下所示:

public class AssemblySkinNameAttribute : Attribute
{
    public string AssemblySkinName { get; set; }

    public AssemblySkinNameAttribute(string assemblySkinName)
    {
        AssemblySkinName = assemblySkinName;
    }
}

public class AssemblySkinDescriptionAttribute : Attribute
{
    public string AssemblySkinDescription { get; set; }

    public AssemblySkinDescriptionAttribute(string assemblySkinDescription)
    {
        AssemblySkinDescription = assemblySkinDescription;
    }
}

在“Skins”目录中找到的每个 DLL 都会被评估,以发现它是否被这些属性装饰;如果存在,则该 DLL 被视为一个皮肤并添加到可供用户选择的可用皮肤列表中。此外,这些属性的内容会被提取并用于 UI 中,以识别不同的皮肤。这在 SkinsFinder 类中的 IsSkin(FileInfo file) 方法中执行。此方法如下所示:

private bool IsSkin(FileInfo file)
{
    bool isSkin = false;
    Assembly skinAssembly = Assembly.LoadFrom(file.FullName);
    AssemblySkinNameAttribute[] skinNames = null;
    AssemblySkinDescriptionAttribute[] skinDescriptions = null;

    try
    {
        skinNames = (AssemblySkinNameAttribute[])
          skinAssembly.GetCustomAttributes(typeof(AssemblySkinNameAttribute), true);
        skinDescriptions = (AssemblySkinDescriptionAttribute[])
          skinAssembly.GetCustomAttributes(typeof(AssemblySkinDescriptionAttribute), true);
    }
    catch (Exception ex)
    {
        // log it
    }

    if (skinNames.Length == 1 && skinDescriptions.Length == 1)
    {
        _skinFriendlyName = skinNames[0].AssemblySkinName;
        _skinDescription = skinDescriptions[0].AssemblySkinDescription;
        isSkin = true;
    }
    return isSkin;
}

因此,一旦完成此 DLL 评估并且所有皮肤都已加载,我们就有一组可供我们的应用程序使用的皮肤。正如您在上文的设置管理部分中所记得的,ISkinManager 用作设置视图中组合框的 DataContext。该组合框的 ItemSource 属性绑定到 SkinManagerSkins 属性。SkinManager 还继承自 DependencyObject,因为 CurrentSkin 属性是一个 DependencyProperty。我应该指出,实际的皮肤加载机制不是我自己的。它是由 Tomer Shamam 在另一个 CodeProject 文章中提供的。您可以阅读他的文章,并在 Tomer 此处提供的代码中看到原始实现,这确实是一项出色的工作。此 DependencyProperty 的完整实现如下:

public Skin CurrentSkin
{
    get { return (Skin)GetValue(CurrentSkinProperty); }
    set { SetValue(CurrentSkinProperty, value); }
}
    
public static readonly DependencyProperty CurrentSkinProperty =
                   DependencyProperty.Register(
                   "CurrentSkin",
                   typeof(Skin),
                   typeof(SkinManager),
                   new UIPropertyMetadata(Skin.Null, OnCurrentSkinChanged, 
                                          OnCoerceSkinValue));
                   
private static object OnCoerceSkinValue(DependencyObject d, object baseValue)
{
    if (baseValue == null)
    {
        return Skin.Null;
    }
    return baseValue;
}

private static void OnCurrentSkinChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
    try
    {
        Skin oldSkin = e.OldValue as Skin;
        oldSkin.Unload();
        Skin newSkin = e.NewValue as Skin;
        newSkin.Load();
    }
    catch (SkinException ex)
    {
        // log it 
    }
}

WPF 主题可能是大量的 XAML 代码,分布在 DLL 中的许多文件中。在一个商业应用程序中,我曾经工作过,主题 DLL 包含数千行 XAML 代码,用于设置应用程序的样式。这需要大量的组织才能使其易于维护。在此应用程序演示中,我创建了两个简单的皮肤,一个“默认”皮肤和一个“蓝色”皮肤。这两个 DLL 之间唯一的实际区别在于颜色。显然,皮肤化应用程序的想法可以走向极端,应用程序根据所选皮肤呈现完全不同的外观。

我根据控件/元素的目标来组织 XAML 文件。还有一个 XAML 文件包含每个皮肤中最重要颜色的绝大部分,以便通过访问一个资源文件轻松重新分配这些颜色。主题 DLL 中包含的文件是

  • ButtonStyle.xaml
  • Colours.xaml
  • ComboBoxStyle.xaml
  • ListBoxStyle.xaml
  • ScrollBarStyle.xaml
  • ShutDownButtonStyle.xaml
  • StatusBarStyle.xaml
  • TextBoxStyle.xaml
  • ToolBarStackPanelStyle.xaml
  • WindowStyle.xaml

在下面的 XAML 中,您可以找到一些您可能希望将其视为重要的颜色示例,这些颜色在保持应用程序外观和感觉一致性方面发挥着作用;此外,还包含一个来自 TextBox 样式的很小的部分,展示了其中一种重要颜色的使用:

<!-- The main application Background Colour -->
    <SolidColorBrush x:Key="AppBackground" Color="#FF434343"/>
    
    <!-- Application Default Text Colour -->
    <SolidColorBrush x:Key="DefaultText" Color="#FFFFE883"/>

    <Style x:Key="JamSoftTextBoxStyle" TargetType="{x:Type TextBox}">
        <Setter Property="BorderBrush" Value="#FFA5ACB2"/>
        <Setter Property="Foreground" Value="{DynamicResource DefaultText}"/>
        <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>

您可以在下面的两个屏幕截图中看到这两个主题的实际效果。您会注意到此应用程序使用了 WPF 使之易于应用的漂亮无边框窗口。为了使窗口可拖动,包含的一个小技巧实际上非常简单:

在 shell 窗口中,我们使用 MouseLeftButtonDown

<Window 
    x:Class="JamSoft.CALDemo.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cal="http://www.codeplex.com/CompositeWPF"
    AllowsTransparency="True"
    WindowStyle="None" 
    MouseLeftButtonDown="Window_MouseLeftButtonDown"
    WindowStartupLocation="CenterScreen"
    Width="800"
    Height="600"
    Style="{DynamicResource JamSoftWindowStyle}" >

然后在 shell 窗口的代码隐藏中,我们有:

    private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    this.DragMove();
}

因此,我们现在拥有一个漂亮的、可换肤的无窗口应用程序,我们还可以像普通的带边框窗口一样拖动它。我们还可以放入新的皮肤 DLL,并让用户可以使用它们,而无需重新编译任何内容。相当不错。

BlueTheme.jpg

DefaultTheme.jpg

动态工具栏

演示应用程序的此版本还允许模块在视图显示在应用程序的主区域时动态地将控件添加到工具栏。同样,这不是将要用于生产应用程序的代码,但它有助于说明可以如何完成这项工作。为了支持这一点,在 JamSoft.CALDemo.PageManager.Core 中包含的原始 IPage 接口中添加了一个附加属性。此接口现在如下所示:

public interface IPage
{
    string ID { get; }
    float Position { get; }
    object View { get; }
    bool IsActivePage { set; }
}

新属性是一个布尔值,称为 IsActivePage。非常不言自明。那么,我们如何利用这一点并执行所需任务呢?我们如何被通知一个页面是激活的还是非激活的?我们使用事件聚合器!我们已经有一个正在触发的事件提供了这个;我们所要做的就是确保任何需要响应激活或非激活的页面订阅此事件并执行相关任务。唯一执行这种动态行为的模块是 MusicSearch 模块。因此,在 MusicSearchPresenter 中,我们像这样订阅 PageSelectedEvent

_eventAggregator.GetEvent<PageSelectedEvent>().Subscribe(
        OnPageSelected, ThreadOption.UIThread);

现在,因为音乐搜索页面的模型是一个相当大的模型,它已经被分离成一个独立的模型。该模型有两个方法,当页面被选中时会激活和停用它,因此我们的 OnPageSelected 处理程序如下所示:

private void OnPageSelected(IPage page)
{
    if (page == this)
    {
        _model.ActivateModel();
    }
    else
    {
        _model.DeactiveModel();
    }
}

现在,我们有一个页面可以被告知它是活动页面,并且可以在它显示在主区域或从主区域移除时执行任务。在我们的特定情况下,当模型被激活时,我们创建一个绑定到 DelegateCommand 的按钮,该按钮将音乐搜索视图重置为新的艺术家搜索过程。我们使用 IToolBarPresentationModel 的引用来在工具栏中注册一个控件。ToolBarPresentationModel 包含两个方法来提供此功能,如下所示:

public void AddToolBarItem(Control control)
{
    if (!_view.DynamicToolPanel.Children.Contains(control))
    {
        _registeredToolControls.Add(control);
        _view.DynamicToolPanel.Children.Insert(0, control);
        NotifyPropertyChanged("RegisteredToolControls");
    }
}

public void RemoveToolBarItem(Control control)
{
    if (_view.DynamicToolPanel.Children.Contains(control))
    {
        _registeredToolControls.Remove(control);
        _view.DynamicToolPanel.Children.Remove(control);
        NotifyPropertyChanged("RegisteredToolControls");
    }
}

这就是工具栏的全部内容。这是一个极其简单的实现,在投入生产应用程序之前需要大量工作,但它很好地说明了如何使用 CAL 来完成事情。

CAL 接口

自开始为第一部分提供的演示应用程序编写代码以来,我一直在思考这个主题。将其写成黑白印刷品非常棘手。我见过人们错误地使用 CAL 应用程序中的接口,并希望在此处包含一些可能有助于澄清某些问题的内容。

我看到的主要问题之一是通过定义 CAL 接口,就像它们是“普通”接口一样。如果您要定义一个自定义集合,您可能会实现 IList 接口,或者在资源密集型情况下,您可能会在对象上实现 IDisposable 以确保清理发生。关键点是这些接口旨在重用,如下面的图所示:

PolymorphicInterfaces.jpg

因此,这里我们有一个 IMyNormalInterface,它由 MyObjectAMyObjectBMyObjectC 实现。然后我们定义了一个可以容纳任何这些对象的集合,因为它们都实现了 IMyNormalInterface。这种行为通常被称为 多态性。相比之下,我们为 CAL 创建的接口类型不适合这种行为。它们通常与单个类型“配对”,并作为一对注册到容器中,以便以后从容器中解析。考虑这个例子:

public interface IPresenterA
{
    void DoSomething();
}

public class PresenterA : IPresenterA
{
    public MyPresenter()
    {
    }
    
    public void DoSomething()
    {
        // does something ...
    }
}

这将被注册到容器中,如下所示:

_container.RegisterType<IPresenterA, PresenterA>(new ContainerControlledLifetimeManager());

IPresenterA 永远只会由具体类型 PresenterA 实现,所以即使我们使用接口,其原因也不是为了允许它以多态方式使用,也不是为了确保 PresenterA 遵循一组特定的功能。即便如此,如果我们的具体类型未能实现接口上的某些内容,我们显然仍会收到编译器错误,但这并不是接口使用的主要驱动力。IPresenterA 在 CAL 上下文中的主要用途是以松散耦合的方式将 PresenterA 的内部暴露给世界的其余部分。这里有另一个图表来说明这一点:

CALInterfaces.jpg

因此,在此图表中,我们展示了在容器中注册的两个演示者,第二个 IPresenterB 请求 IPresenterA 的引用,因为它需要能够调用在 IPresenterA 接口上定义并在 PresenterA 类型上实现的 DoSometing() 方法。

如果开发人员设计 CAL 接口的方式与设计普通接口相同,您可能会暴露该类型能够做的所有事情,而无需需要或真正想要这样做。您还可能冒着通过公开一个同时也作为 EventAggregator 事件处理程序的事件处理程序等方式来破坏严格通信方法的风险。

总结

我认为这基本上就是这个演示应用程序和系列文章的全部内容。我希望人们觉得它有用,并从中获得一些想法用于他们自己的应用程序。感谢阅读。

© . All rights reserved.