Разработка приложения Hello MEF — Часть II – Метаданные

Еще один перевод статьи Глена Блока на тему Managed Extensibility Framework Building Hello MEF – Part II – Metadata and why being Lazy is a good thing.

В первой части этой серии статей мы создали основу нашего приложения, в конце прошлой статьи оно показало один виджет. В этой статье мы покажем два виджета, sensing a pattern here? ((sensing a pattern here?))  :-) Мы покажем два, но поместим их в разные места приложения. Мы изучим два способа сделать это, включая очень мощный механизм MEF называемый метаданные.

Если вы только перешли к этой статье вы можете взять исходные коды проекта здесь

Добавление второго виджета

Теперь когда мы получили инфраструктуру нашего приложения, добавление второго виджета должно быть легко, верно? Давайте сделаем это, на этот раз мы добавим текстовое поле.
Первым делом мы создадим новый пользовательский контрол с именем Widget2.

Далее мы перейдем в Widget2.xaml и добавим кнопку с содержимым «Hello MEF».

<UserControl x:Class="HelloMEF.Widget2"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"> 
    <Grid x:Name="LayoutRoot" Height="Auto">
        <TextBox Text="Hello MEF!" TextAlignment="Center" Height="100" Width="300"
    </Grid>
</UserControl>

Затем перейдем в Widget2.xaml.cs и добавим наш экспорт.

using System;
using System.ComponentModel.Composition;
using System.Windows.Controls;
using System.Windows;

namespace HelloMEF
{
    [Export(typeof(UserControl))]
    public partial class Widget1 : UserControl
    {
        public Widget1()
        {
            InitializeComponent();
        }
    }
}

Все что мы сделали это создали новый контрол и добавили атрибут экспорта для MEF. Давайте запустим приложение.

Presto! Показался и второй виджет. Никакого редактирования app.config, никакого явного кода по регистрации. Просто добавили его в проект и все. Неплохо!

Добавление второго виджета куда нам захочется, и почему Lazy это хорошо

Выглядит очень хорошо. Но если вы читали мою первую статью, то вы помните что мы хотим показывать виджеты в разных местах потому что у нас есть две панели на экране для виджетов. Одна из них голубая, и одна желтая, но сейчас она невидимая. Так, что мы должны сделать? Один из вариантов это создать разные интерфейсы, ITopWidget, IBottomWidget. Тогда мы можем проверять класс если он реализует один из этих интерфейсов, мы поместим его в соответствующее место.

Если мы сделаем это, то наш код в MainPage будет выглядеть так:

public MainPage()
{
    InitializeComponent();
    PartInitializer.SatisfyImports(this);
    foreach (var widget in Widgets)
    {
        if (widget is ITopWidget)
            TopWidgets.Items.Add(widget);
        else if (widget is IBottomWidget)
            BottomWidgets.Items.Add(widget);
    }
}

Это работает, однако этот способ принуждает нас создавать новые интерфейсы каждый раз когда мы захотим модернизировать пользовательский интерфейс (UI). Например, если мы захотим добавить панель с левой стороны, то мы должны создать для этого интерфейс ILeftWidget. Другой проблемой является, то что мы создаем виджеты не реализующие ни один из наших интерфейсов. И что нам с этим делать?

«Ленивая» инициализация (Lazy)

Ответ на этот вопрос — Lazy. Да друзья, это первый раз когда Lazy оказалось полезным. Если вы еще не знакомы, Lazy это новый тип в SL4 и FX4 BCL который позволяет вам отложить создание экземпляра класса. MEF полностью поддерживает Lazy, таким образом вы можете «лениво» (отложено) импортировать отдельные значения или коллекции. Для примера мы можем изменить наш атрибут ImportMany для использования Lazy, вставив следующий код в MainPage.cs:

using System;
using System.Windows.Controls;
using System.ComponentModel.Composition;

namespace HelloMEF
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            PartInitializer.SatisfyImports(this);
            foreach (var widget in Widgets)
            {
                TopWidgets.Items.Add(widget.Value);
            }
        }
        [ImportMany]
        public Lazy<UserControl>[] Widgets { get; set; }
    }
}

Выше вы должны были заметить две вещи, во-первых наши виджеты теперь импортируются в массив типов Lazy, во-вторых цикл в котором мы добавляем виджеты. Value rather than the widget value directly. ((Value rather than the widget value directly)) Здесь вместо импорта экземпляров UserControl мы импортируем «ленивые» ссылки на наши виджеты. При первом обращении к значению, создается экземпляр UserControl к которому мы и получаем доступ к значению для добавления виджета в ItemControl TopWidget`а.

Metadata

Ну вот все хорошо, потому что сейчас мы отложили создание контролов до тех пор пока они нам не понадобятся. Недостатком этого является то что мы не знаем как определить какой из виджетов и куда мы должны его разместить. И это то место где метаданные смогут нам помочь. MEF поддерживает не только «ленивое» создание экземпляров, но он так же поддерживает концепцию экспорта метаданных. Это означает что экспорт MEF может дать дополнительную информацию о себе, которую импортер может использовать различными способами, будь то определение должен ли быть создан объект, или в нашем случае определить где он должен показываться на экране.
Импортер может получить доступ к этой информации до того как экземпляр фактически будет активирован. Это означает что если специфический экспорт не соответствует требованиям, то он не должен быть создан.

Метаданные в MEF условны, это означает что вы можете использовать их как вы считаете необходимым в соответствии с потребностями вашего приложения. Использование метаданных состоит из двух частей, первая это та в которой экспортер определяет какие метаданные будут доступны для просмотра импортеру. Затем, импортер в момент импорта может получить доступ к метаданным.

Определение метаданных для виджетов

В нашем случае мы хотим чтобы наши виджеты предоставляли нам свое положение. Для того, чтобы сделать это, мы создадим новое перечисление (enum) с именем WidgetLocation.

namespace HelloMEF
{
    public enum WidgetLocation {Top, Bottom}
}

Затем, мы добавим метаданные Location в наши виджеты. Тут есть несколько способов сделать это с помощью MEF, самый простой из которых это начать с простого добавления атрибута ExportMetadata. Сначала перейдем в Widget1.xaml.cs и установим положение виджета сверху формы, как вы можете увидеть ниже. Предупреждение, ключ и значение могут быть произвольными, в моем случае это «Location» и «WidgetLocation.Top»

    [ExportMetadata("Location", WidgetLocation.Top)]
    [Export(typeof(UserControl))]
    public partial class Widget1 : UserControl
    {
        public Widget1()
        {
            InitializeComponent();
        }
    }

Далее мы перейдем к Widget2 и установим его положение снизу формы.

    [ExportMetadata("Location", WidgetLocation.Bottom)]
    [Export(typeof(UserControl))]
    public partial class Widget2 : UserControl
    {
        public Widget2()
        {
            InitializeComponent();
        }
}

Теперь мы можем перейти к импортеру.

Предоставление доступа виджета к панели и почему у Lazy есть брат

Теперь, когда мы определили метаданные которые хотим использовать. Мы поговорили о Lazy, теперь разговор будет о его брате Lazy. В порядке предоставления нам доступа к метаданным, MEF представляет специальный класс Lazy, который может содержать метаданные. М в данном случае это интерфейс (мы назовем его представление метаданных) который содержит только методы получения свойств, где имя свойство соответствует значению ключа в метаданных. MEF автоматически создает прокси класс который реализует этот интерфейс и может содержать все метаданные. Это очень хорошо! Вы должны посмотреть на это чтобы поверить (и понять).

Так что давайте просто сделаем это, чем говорить об этом.

Первое, мы должны создать новый интерфейс с именем IWidgetMetadata который будет содержать метаданные положения (Location).

namespace HelloMEF
{
    public interface IWidgetMetadata
    {
        public WidgetLocation Location {get;}
    }
}

Далее мы перейдем в MainPage.xaml.cs, и изменим наш импорт виджетов что бы использовать новые метаданные. Так же, мы должны изменить логику расположения виджетов в подходящих местах.

using System;
using System.Windows.Controls;
using System.ComponentModel.Composition;

namespace HelloMEF
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            PartInitializer.SatisfyImports(this);
            foreach (var widget in Widgets)
            {
                if (widget.Metadata.Location == WidgetLocation.Top)
                    TopWidgets.Items.Add(widget.Value);
                else if (widget.Metadata.Location == WidgetLocation.Bottom)
                    BottomWidgets.Items.Add(widget.Value);
            }
        }

        [ImportMany]
        public Lazy<UserControl,IWidgetMetadata>[] Widgets { get; set; }
    }
}

Выше вы можете увидеть что теперь тип виджетов стал Lazy. К тому же, в пределах цикла у нас есть безопасный для компиляции доступ к widget.Metadata.Location, но теперь мы никогда не сможем создать конкретный класс который реализует IWidgetMetadata, не так ли? Мы не сможем, но MEF сможет :-) Результат не содержит магических строк на стороне импортера, мы получили intellisense, рефакторинг и проверку на этапе компиляции! Так же, обратите внимание мы по прежнему должны получить доступ к значению (widget.Value), что бы получить созданный виджет. На самом деле это преимущество, хотя если мы получим виджет в другом месте, это не будет расточительно. Здесь это не имеет большой разницы, но если мы имели бы 50 виджетов это была бы уже другая история.

Теперь давайте запустим приложение.

Вуаля, мы видим наши виджеты отобразились в нужном месте, и нам не нужно делать сальто назад 1000 раз.

Магические строки зло! И MEF этому ответ.

Обратите внимание, выше я упоминал что мы получаем все это безопасно для компиляции и поддержку intellisense для импортера. Но что насчет экспортера? Наши видежты все еще должны указывать магические строки для ключей. Не только это, но представьте если я имею метаданные от многих элементов, которые представлены отнюдь не одной строкой для каждого элемента. Плюс ко всему я теряю понятность кода, потому что я не знаю как интуитивно создать виджет, и какой смысл имеют метаданные. Даже если мы создадим константы для ключей мы не будем знать тип виджетов.

Было бы очень красиво если бы мы могли сделать это следующим образом: [ExportWidget(Location=WidgetLocation.Top)], не так ли? Нам больше не нужен будет Export, а так же все атрибуты ExportMetadata. Это сделает наш код более чистыми и менее шумным.
Хорошая новость, вы можете так сделать.

Пользовательские экспорты

MEF позволяет вам сделать собственный экспорт, который содержит метаданные. Собственный экспорт это ваш собственный атрибут с метаданными, и он строго типизированный.

Давайте создадим наш новый атрибут ExportWidgetAttribute.

using System;
using System.Windows.Controls;
using System.ComponentModel.Composition;

namespace HelloMEF
{
    [MetadataAttribute]
    [AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
    public class ExportWidgetAttribute : ExportAttribute
    {
        public ExportWidgetAttribute()
            :base(typeof(UserControl))
        {
        }

        public WidgetLocation Location { get; set; }
    }
}

Если посмотрим выше, стоит отметить некоторые вещи:
1. ExportWidgetAttribute наследует ExportAttribute. Это то что говорит MEF «Эй, я атрибут экспорта».
2. Базовый конструктор атрибута передается тип UserControl. Этот шаг является критическим, иначе MEF будет использовать конкретный тип на который установлен атрибут.
3. Атрибут имеет установленный атрибут MetadataAttribute, это говорит MEF, что это не только экспорт, но так же предоставляет метаданные. Теперь когда он применился, MEF будет искать каждое публичное свойство атрибута которое я добавил, и будет получать из них метаданные, ключем для которых будет имя свойства. В сущности это добавит [ExportMetadata(«Location»,…)] с значением присвоенным свойству.
4. Атрибут имеет установленный атрибут [AttributeUsage], указывающий допустимые цели для него, в данном случае классы и так что AllowMultiple = false. Если мы не сделаем это, тогда MEF будет принимать множество наборов метаданных которые существуют и будет возвращать метаданные как массив.

Теперь мы можем изменить каждый из наших виджетов для использования нового атрибута.

Первый Widget1.xaml.cs

using System.Windows.Controls;

namespace HelloMEF
{
    [ExportWidget(Location=WidgetLocation.Top)]
    public partial class Widget1 : UserControl
    {
        public Widget1()
        {
            InitializeComponent();
        }
    }
}

И затем Widget2.xaml.cs

using System.Windows.Controls;

namespace HelloMEF
{
    [ExportWidget(Location=WidgetLocation.Bottom)]
    public partial class Widget2 : UserControl
    {
        public Widget2()
        {
            InitializeComponent();
        }
    }
}

Наш собственный атрибут выглядит очень хорошо. Не надо больше указывать контракт, нет больше лишних метаданных, и выглядит очень красиво и компактно. Так же стоит заметить, что наш раздел using включает System.ComponentModel.Composition. Это означает что наши потребители могут использовать MEF даже незнаю что они используют его. Мы только посылаем им наши атрибуты и интерфейсы, и они могут начинать.

Нажмите магическую кнопку Run и вот что вы получите.

Вопросы? :-)

Краткое изложение

В этой статье мы изучили как можем использовать метаданные и Lazy что бы делать красивые вещи с помощью MEF. Мы можем снабжать экспорт информацией, которую импортеры могут использовать чтобы определить могут ли они использовать фрагменты, и как они могут сделать это (например поместить фрагмент в верхнее положение). Мы так же узнали, как можно создать собственный атрибут экспорта.

Что дальше?

В следующей статье мы рассмотрим как разделить наши приложения на несколько XAP файлов и использовать MEF для загрузки их по требованию. Мы так же узнаем еще несколько аспектов MEF, которые не столь очевидны.

There’s much more to come after that, the series is shaping up!