C# + WPF + MEFを使ってアプリにプラグインを実装してみる その1 

#はじめに
わたしも現在勉強中で、間違っていることもあるかもしれません。
ここが違う！とか、こうしたほうがよくね？ってのがありましたら教えてください。

GitHubに上げました！
https://github.com/isuzu-shiranui/CalculationApplication

# MEFとは
Managed Extensibility Framework.
.NET Framework 4から導入された、プラグインとか拡張機能とか言われているものを実装するためのフレームワークです。
私は「メフ」って呼んでます。

(依存性の注入については割愛)

# 実際に作ってみる
今回は、軽く足し算を行うプラグインを考えてみます。
また、画面のプラグイン化は次回行うとして、足し算のロジック部分だけ分離したいと思います。

本開発はPrismを使ったMVVMでの開発となりますのでNugetで以下3つを参照してください。

- Prism.Core v6.1.0
- Prism.Wpf v6.1.0
- Prism.Mef v6.1.0

# 下準備
まずは読み込み側の基礎部分をさくっと作っていきます。
ただ、ViewとViewModelのブーストラッパは今回使用していません。
## View (MainWindow)
### 外観
![c3cf609f1a93206ecf9ed6290e1ee357.png](https://qiita-image-store.s3.amazonaws.com/0/133834/a65a3081-b085-b972-279e-56b8cd87cfe1.png)


###コード
Views\MainWindow.xaml

```xml
<Window x:Class="CalculationApplication.Views.MainWindow"
        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"
        xmlns:vm="clr-namespace:CalculationApplication.ViewModels"
        xmlns:local="clr-namespace:CalculationApplication.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="527.298">

    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    
    <Grid>
        <Grid>
            <StackPanel HorizontalAlignment="Left" Height="24" Margin="10,113,0,0" VerticalAlignment="Top" Width="499" Orientation="Horizontal">
                <Label Content="左辺" />
                <TextBox Width="212" Text="{Binding LeftValue, Mode=TwoWay}"/>
                <Label Content="左辺" />
                <TextBox Width="212" Text="{Binding RightValue, Mode=TwoWay}"/>
            </StackPanel>
            <Button Content="実行" HorizontalAlignment="Left" Margin="425,239,0,0" VerticalAlignment="Top" Width="75" Command="{Binding CalculationCommand, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" Margin="44,172,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="211" Text="{Binding ResultValue}"/>
            <Label x:Name="label" Content="結果" HorizontalAlignment="Left" Margin="10,172,0,0" VerticalAlignment="Top"/>
        </Grid>
    </Grid>
</Window>


```
Views\MainWindow.xaml.cs

```csharp
using System.Windows;

namespace CalculationApplication.Views
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}
```
##ViewModel (MainWindowViewModel)
ViewModels\MainWindowViewModel.cs

```csharp
using Prism.Commands;
using System.ComponentModel.DataAnnotations;

namespace CalculationApplication.ViewModels
{
    public class MainWindowViewModel : Commons.ViewModelBase
    {
        private double leftValue;
        [Required]
        [Range(0, double.MaxValue)]
        public double LeftValue
        {
            get { return leftValue; }
            set { SetProperty(ref leftValue, value); CalculationCommand.RaiseCanExecuteChanged(); }
        }

        private double rightValue;
        [Required]
        [Range(0, double.MaxValue)]
        public double RightValue
        {
            get { return rightValue; }
            set { SetProperty(ref rightValue, value); CalculationCommand.RaiseCanExecuteChanged(); }
        }

        private double resultValue;
        public double ResultValue
        {
            get { return resultValue; }
            set { SetProperty(ref resultValue, value); }
        }


        private DelegateCommand calculationCommand;
        public DelegateCommand CalculationCommand => calculationCommand ?? (new DelegateCommand(CalculationExecute, () => !HasErrors));

        private void CalculationExecute()
        {
            
        }
    }
}

```

## ViewModelBase
Commons\ViewModelBase.cs

```csharp
using Prism.Mvvm;
using System;
using System.Runtime.CompilerServices;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace CalculationApplication.Commons
{
    public class ViewModelBase : BindableBase, INotifyDataErrorInfo
    {
        private ErrorsContainer<string> errorsContainer;

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public ViewModelBase()
        {
            errorsContainer = new ErrorsContainer<string>(propertyName => ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)));
        }

        public bool HasErrors => errorsContainer.HasErrors;

        public IEnumerable GetErrors(string propertyName)
        {
            return errorsContainer.GetErrors(propertyName);
        }

        protected override bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if(!base.SetProperty<T>(ref storage, value, propertyName)) return false;

            var context = new ValidationContext(this) { MemberName = propertyName };
            var errors = new List<ValidationResult>();
            if(!Validator.TryValidateProperty(value, context, errors))
            {
                errorsContainer.SetErrors(propertyName, errors.Select(error => error.ErrorMessage).ToArray());
            }
            else
            {
                errorsContainer.ClearErrors(propertyName);
            }
            return true;
        }
    }
}
```

##App.xaml

 ```StartupUri="MainWindow.xaml"``` →  ```StartupUri="Views/MainWindow.xaml"```に変更

#プラグイン対応のための実装
##インターフェースを作る
これを作ることで、読み込むプラグインをある意味フィルタリングできます。
同じソリューション内で新しくクラスライブラリプロジェクトを作ってください。
![92087267a388b691daf2280ac0ea05de.png](https://qiita-image-store.s3.amazonaws.com/0/133834/a8e78e2c-e66c-3b8f-6572-c296201d6284.png)

そして元あるクラスを消して、新しくインタフェースを下記のように実装してください。
### IPlugin

```csharp
namespace Plugin
{
    public interface IPlugin
    {
        double Calculation(double leftValue, double rightValue);
    }
}
```

## 読み込み側を対応させる
下記に追加をします。
usingの部分は新たに参照に追加してください。
### MainWindowViewModel

```csharp
using Plugin;
using System.ComponentModel.Composition;
```

```csharp

[Import(typeof(IPlugin))]//プラグインを"1つだけ"読み込む
private IPlugin Plugin { get; set; }

private void CalculationExecute()
{
     ResultValue = Plugin.Calculation(LeftValue, RightValue);
}

public MainWindowViewModel()
{
　　　LoadPlugins();
}

/// <summary>
/// プラグインを全て読みこんで、[Import]がついているプロパティに格納する。
/// </summary>
private void LoadPlugins()
{
    　//フォルダがなければ作る。
      string pluginsPath = Directory.GetCurrentDirectory() + @"\plugins";
      if (!Directory.Exists(pluginsPath)) Directory.CreateDirectory(pluginsPath);

      //プラグイン読み込み
      using (var catalog = new DirectoryCatalog(pluginsPath))
      using (var container = new CompositionContainer(catalog))
      {
          f (catalog.LoadedFiles.Count > 0) container.SatisfyImportsOnce(this);
      }
}
```

#プラグインを作る
それでは足し算するだけのプラグインを作ります。
インターフェースを作ったのと同じように、新たにクラスライブラリプロジェクトを作ってください。
そして、そこにクラスを作って下記のように記述します。

```csharp
using Plugin;
using System.ComponentModel.Composition;

namespace SummationPlugin
{
    [Export(typeof(IPlugin))]
    public class Summation : IPlugin
    {
        public double Calculation(double leftValue, double rightValue)
        {
            return leftValue + rightValue;
        }
    }
}
```

# 動かしてみる
## プラグインを配置する
まずは作ったプラグインをビルドします。
すると、プラグインのDebugまたはReleaseフォルダに、(プラグイン名).dllができていると思います。
![a6dfabaf001e9996c26dbcde0fada368.png](https://qiita-image-store.s3.amazonaws.com/0/133834/b0325403-5c6d-df9b-e7d6-9fbf52c94764.png)
これを、読み込みアプリのDebugまたはReleaseフォルダにpluginフォルダがなければ作って貼り付けます。
![849e7f8c3e71b3d46697e8b580b33607.png](https://qiita-image-store.s3.amazonaws.com/0/133834/8d28b8fd-9330-df26-c2eb-0048207c9413.png)

## 読み込み側アプリの動作確認
実際にアプリを起動して、計算が行われれば完成です。お疲れ様でした！
![45ba1eefcb45d4df5e0c684388cf4b84.gif](https://qiita-image-store.s3.amazonaws.com/0/133834/809c22e6-6cd7-0185-e16b-6423b60fd751.gif)

冒頭にもあるように、GitHubにあげています。
https://github.com/isuzu-shiranui/CalculationApplication
一部別のフォルダも紛れてしまいました...申し訳ありません。