AutoMapperを使ってオブジェクト間のデータコピーを自動化するには?(基本編).NET TIPS

オープンソースのライブラリ「AutoMapper」を使い、オブジェクト間でのデータのコピーという煩雑な処理を数行で実現する方法を解説する。

» 2015年03月17日 17時15分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4以降


 あるオブジェクトのプロパティ値を別のオブジェクトのプロパティ値にコピーするコードは、よく書くものだ。特にMVVMなどのアーキテクチャでロジックと画面を分離するときには、頻繁に記述することになる。そのようなコードの記述は退屈だし、それ故、間違いも入り込みやすい。自動化できたらよいのにと思ったことはないだろうか? それを実現する「AutoMapper」というオープンソースのライブラリがある。本稿では、AutoMapperの基本的な使い方を紹介する。

 なお、本稿執筆時点でAutoMapperがサポートしているのは.NET Framework 4以降であるが、NuGetから導入するため、本稿ではVisual Studio 2012を使って説明する*1。また、本稿のサンプルは「MSDN Code Recipe:.NET Tips #1102」からダウンロードできる。

*1 NuGetから導入する機能はVisual Studio 2012から標準搭載になった。Visual Studio 2010でNuGetを利用したい場合は、「特集:.NET開発の新標準「NuGet」入門(前編)〜.NETで開発モジュール導入が楽々に! NuGet入門」を参考にしてほしい。


AutoMapperを導入するには?

 Visual StudioのプロジェクトごとにNuGetから導入する(次の画像)。

NuGetからAutoMapperをインストールする(Visual Studio 2012) NuGetからAutoMapperをインストールする(Visual Studio 2012)
ソリューションエクスプローラーでプロジェクトを選択し、その右クリックメニューから[NuGet パッケージの管理]を選ぶ((1))。
[NuGet パッケージの管理]ダイアログで、左側の[オンライン]を選び((2))、右上の検索ボックスに「AutoMapper」と入力して検索する((3))。見つかったAutoMapperを選ぶと[インストール]ボタンが表示されるので((4))、それをクリックしてインストールする。
なお、右側の[ライセンス条項の表示]リンク((5))でライセンス条件を確認できるので、利用に先立って確認しておいてほしい。

 AutoMapperの基本的な機能は、二つのクラスの間で同じ名前のプロパティを見つけ出し、それらのプロパティ値をコピーすることである。以下、例題を示した上で、基本的な使い方を説明しよう。

例題

 ここでは例題として、人の情報を格納する「Person」クラスと「PersonForDisplay」クラスという二つのクラスを考えてみよう。

 「Person」クラスはロジックで使うオブジェクトだとする。ロジックで使うオブジェクトは、シンプルなものにしたい。画面の都合に振り回されたくないのである。例えば次のコードのようになる。

public class Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }

  public static IList<Person> GetData()
  {
    var list = new List<Person>();
    // ダミーのデータ(実際にはデータベースなどから取得してくると思ってほしい)
    list.Add(new Person() { LastName = "八洲", FirstName = "未來", });
    list.Add(new Person() { LastName = "山本", FirstName = "康彦", });
    list.Add(new Person() { LastName = "嶺", FirstName = "阿室", });
    return list;
  }
}

Public Class Person

  Public Property FirstName As String
  Public Property LastName As String

  Public Shared Function GetData() As IList(Of Person)
    Dim list = New List(Of Person)()
    ' ダミーのデータ(実際にはデータベースなどから取得してくると思ってほしい)
    list.Add(New Person() With {.LastName = "八洲", .FirstName = "未來"})
    list.Add(New Person() With {.LastName = "山本", .FirstName = "康彦"})
    list.Add(New Person() With {.LastName = "嶺", .FirstName = "阿室"})
    Return list
  End Function
End Class

ロジックで使う「Person」クラスの例(上:C#、下:VB)
「FirstName」と「LastName」という二つのプロパティを持つシンプルなクラスである。
また、何らかの方法でデータを取得してくる「GetData」メソッドがある(このクラスに配置することには異論があるかもしれないが、サンプルということでご容赦願いたい)。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型や、Visual Basic 2008から利用できるようになったオブジェクト初期化子、そしてVisual Basic 2010から利用できるようになった自動実装プロパティを使用している。

 次に、「PersonForDisplay」クラスは、WPFの画面にバインドして表示するためのものだとする。画面には、「FirstName」と「LastName」ではなく、空白を挟んで両方のプロパティ値をつなげた「DisplayName」プロパティを表示したいとしよう。また、データの変更を画面に反映させたいので、INotifyPropertyChangedインターフェース(System.ComponentModel名前空間)も実装したいものとする。すると、次のコードのようにちょっと複雑なクラスになる。

public class PersonForDisplay : System.ComponentModel.INotifyPropertyChanged 
{
  // 公開するイベントハンドラー(INotifyPropertyChangedインターフェースの実装)
  public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
  private void NotifyPropertyChanged(string info)
  {
    var handler = this.PropertyChanged;
    if (handler != null)
      handler(this, new System.ComponentModel.PropertyChangedEventArgs(info));
  }

  // FirstNameプロパティ
  // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  private string _firstName;
  public string FirstName
  {
    get { return _firstName; }
    set
    {
      if (string.Equals(_firstName, value))
        return;
      _firstName = value;
      NotifyPropertyChanged("FirstName");
      NotifyPropertyChanged("DisplayName");
    }
  }

  // LastNameプロパティ
  // 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  private string _lastName;
  public string LastName
  {
    get { return _lastName; }
    set
    {
      if (string.Equals(_lastName, value))
        return;
      _lastName = value;
      NotifyPropertyChanged("LastName");
      NotifyPropertyChanged("DisplayName");
    }
  }

  // 画面に表示したいDisplayNameプロパティ
  public string DisplayName
  {
    get { return string.Format("{0} {1}", LastName, FirstName); }
  }
}

Public Class PersonForDisplay
  Implements ComponentModel.INotifyPropertyChanged

  ' 公開するイベントハンドラー(INotifyPropertyChangedインターフェースの実装)
  Public Event PropertyChanged(sender As Object, e As ComponentModel.PropertyChangedEventArgs) _
    Implements ComponentModel.INotifyPropertyChanged.PropertyChanged
  Private Sub NotifyPropertyChanged(info As String)
    RaiseEvent PropertyChanged(Me, _
                               New System.ComponentModel.PropertyChangedEventArgs(info))
  End Sub

  ' FirstNameプロパティ
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _firstName As String
  Public Property FirstName As String
    Get
      Return _firstName
    End Get
    Set(value As String)
      If (String.Equals(_firstName, value)) Then
        Return
      End If
      _firstName = value
      NotifyPropertyChanged("FirstName")
      NotifyPropertyChanged("DisplayName")
    End Set
  End Property

  ' LastNameプロパティ
  ' 値に変化があったときにはPropertyChangedイベントハンドラーを発火させる
  Private _lastName As String
  Public Property LastName As String
    Get
      Return _lastName
    End Get
    Set(value As String)
      If (String.Equals(_lastName, value)) Then
        Return
      End If
      _lastName = value
      NotifyPropertyChanged("LastName")
      NotifyPropertyChanged("DisplayName")
    End Set
  End Property

  ' 画面に表示したいDisplayNameプロパティ
  Public ReadOnly Property DisplayName As String
    Get
      Return String.Format("{0} {1}", LastName, FirstName)
    End Get
  End Property
End Class

画面で使う「PersonForDisplay」クラスの例(上:C#、下:VB)
ロジックで使う先ほどの「Person」クラスに比べて、かなり長いコードになっている。画面に表示したい「DisplayName」プロパティと、データの変更を画面に反映させるためのINotifyPropertyChangedインターフェースを実装したためだ。
このような複雑なクラスはロジックで使いたくない。また、ロジックと関係のないSystem.ComponentModel名前空間がロジックに入ってくるのも避けたいものだ。そのため、このように「Person」と「PersonForDisplay」の二つのクラスに分けて設計を進めることになる。

 また、画面には「PersonForDisplay」オブジェクトのリストを表示したいとする。デザイン画面での利便性を考えると、「PersonForDisplay」オブジェクトのコレクションを持つ「PersonsList」クラスも必要になる(次のコード)。

public class PersonsList
{
  private System.Collections.ObjectModel.ObservableCollection<PersonForDisplay>
    _persons = new System.Collections.ObjectModel.ObservableCollection<PersonForDisplay>
                    (new List<PersonForDisplay>());
  public System.Collections.ObjectModel.ObservableCollection<PersonForDisplay>
    Persons { get { return _persons; } }

  public PersonsList()
  {
    // デザイン時に表示するダミーデータを設定する
    // ……省略……
  }
}

Public Class PersonsList
  Private _persons _
    As System.Collections.ObjectModel.ObservableCollection(Of PersonForDisplay) _
    = New System.Collections.ObjectModel.ObservableCollection(Of PersonForDisplay) _
            (New List(Of PersonForDisplay)())
  Public ReadOnly Property Persons _
      As System.Collections.ObjectModel.ObservableCollection(Of PersonForDisplay)
    Get
      Return _persons
    End Get
  End Property

  Public Sub New()
    ' デザイン時に表示するダミーデータを設定する
    ' ……省略……
  End Sub
End Class

画面にバインドするための「PersonsList」クラスの例(上:C#、下:VB)
PersonForDisplayオブジェクトを格納するObservableCollectionオブジェクト(System.Collections.ObjectModel名前空間)を、プロパティとして公開している。
WPFのXAMLコードの側でPersonForDisplayオブジェクトのコレクションを生成してバインドするには、このようなクラスを用意しておくとよい。省略した部分でダミーデータを設定すれば、デザイン画面でもデータが表示されるようになる。
なお、コードビハインドでObservableCollectionオブジェクトを生成して画面のコントロールにバインドするのであれば、このクラスは不要だ。しかしそうすると、デザイン画面ではデータが表示されない。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型を使用している。

 この「PersonsList」クラスをWPFのListViewコントロール(System.Windows.Controls名前空間)にバインドして表示させる例を、次の画像に示す。

例題のクラスを使ってListViewにデータを表示した例(Visual Studio 2012) 例題のクラスを使ってListViewにデータを表示した例(Visual Studio 2012)
これはデバッグ実行しているところである。実際のコードは、別途公開のサンプルをご覧いただきたい。
実行画面とデザイン画面で表示が異なるのは、PersonsListクラスのコンストラクターでデザイン時のダミーデータを設定しているからである(前出のコードでは省略している部分)。
なお、このようにリストにフルネームを表示する場合、LastNameクラスをバインドするTextBlockコントロール(System.Windows.Controls名前空間)とFirstNameクラスをバインドするTextBlockコントロールの二つをListViewコントロールのテンプレートに配置する方法もある。ただし、例えば、全てカタカナの名前のときにはFirstNameとLastNameを入れ替えて表示したいといったときには、テンプレートで対処するのは面倒なことになる(PersonForDisplayクラスにDisplayNameプロパティを持たせる方法では、DisplayNameプロパティの中に入れ替え処理を記述するだけで済む)。

AutoMapperを使わない場合

 AutoMapperを使わない場合は、画面が表示されるときなどに、次のコードのようにしてプロパティ値を逐一コピーしなければならない。

// ロジックを呼び出してデータを取得する
IList<Person> data = Person.GetData();

// 表示用のコレクションを用意する
var personsList = new PersonsList();

// データを表示用のコレクションにコピーする(AutoMapper未使用)
foreach (Person p in data)
{
  personsList.Persons.Add(
    new PersonForDisplay() { 
      FirstName = p.FirstName,
      LastName = p.LastName,
      // ここでは二つだけだが、一般にはうんざりするほどの代入文を書くことになる
    }
  );
}

// 表示用のコレクションをデータコンテキストにセット
this.DataContext = personsList;

' ロジックを呼び出してデータを取得する
Dim data As IList(Of Person) = Person.GetData()

' 表示用のコレクションを用意する
Dim personsList = New PersonsList()

' データを表示用のコレクションにコピーする(AutoMapper未使用)
For Each p As Person In data
  personsList.Persons.Add( _
    New PersonForDisplay() _
      With {
        .FirstName = p.FirstName,
        .LastName = p.LastName
      }
  )
  ' ここでは二つだけだが、一般にはうんざりするほどの代入文を書く
Next

' 表示用のコレクションをデータコンテキストにセット
Me.DataContext = personsList

AutoMapperを使わない場合の例(上:C#、下:VB)
これはWPFの画面のコードビハインドに記述する例である。ロジックを呼び出してデータを取得した後、それを表示用のコレクションに詰め替える処理を書く必要がある。ここではコピーするプロパティが二つだけだが、実際にはもっと多いだろう。
なお、このVBのコードでは、Visual Basic 2005からの機能であるジェネリック型や、Visual Basic 2008から利用できるようになったオブジェクト初期化子を使用している。

 上のコードでは、同じ名前のプロパティ値をコピーするコードが何行も続く(サンプルなので2行しかないが、実際にはもっと多いだろう)。AutoMapperを使うことで、この部分を簡潔に書けるのだ。

オブジェクトごとにAutoMapperを使うには?

 先のコードで「データを表示用のコレクションにコピーする」というコメントを付けた部分は、AutoMapperを使うと次のコードのように簡潔に書ける。

// AutoMapperを使う(オブジェクトごと)
AutoMapper.Mapper.CreateMap<Person, PersonForDisplay>();
foreach (Person p in data)
  personsList.Persons.Add(
    AutoMapper.Mapper.Map<PersonForDisplay>(p)
  );

' AutoMapperを使う(オブジェクトごと)
AutoMapper.Mapper.CreateMap(Of Person, PersonForDisplay)()
For Each p As Person In data
  personsList.Persons.Add( _
    AutoMapper.Mapper.Map(Of PersonForDisplay)(p))
Next

AutoMapperを使ってオブジェクトごとにデータをコピーする例(上:C#、下:VB)
従来の書き方ではプロパティの数だけ書いていたデータコピーのコードが、AutoMapperを使うと2行だけになる(CreateMapメソッド呼び出しとMapメソッド呼び出しの2行)。また、AutoMapperはコピー先のオブジェクト(ここではPersonForDisplayクラスのインスタンス)を生成してくれるので、コンストラクターの呼び出しも不要になっている。

 AutoMapperを使うには、まずMapperクラス(AutoMapper名前空間)のCreateMapメソッドを呼び出して、コピー元とコピー先のクラスを登録する。型引数は、コピー元/コピー先の順だ。

 実際にオブジェクトをコピーするには、MapperクラスのMapメソッドを使う。Mapメソッドの型引数はコピー先のクラスである。Mapメソッドの引数には、コピー元のオブジェクトを渡す。すると、Mapメソッドはコピー先のオブジェクトを生成し、同じ名前のプロパティの値をコピーして返してくれる。

コレクション丸ごとにAutoMapperを使うには?

 AutoMapperは、コレクションのコピーにも対応している。CreateMapメソッドの呼び出し方は前と同じだが、Mapメソッドを呼び出すときにコレクションを渡せばよい(次のコード)。

// AutoMapperを使う(コレクション丸ごと)
AutoMapper.Mapper.CreateMap<Person, PersonForDisplay>();
var items = AutoMapper.Mapper.Map<IList<PersonForDisplay>>(data);
foreach (PersonForDisplay item in items)
  personsList.Persons.Add(item);

' AutoMapperを使う(コレクション丸ごと)
AutoMapper.Mapper.CreateMap(Of Person, PersonForDisplay)()
Dim items = AutoMapper.Mapper.Map(Of IList(Of PersonForDisplay))(data)
For Each item As PersonForDisplay In items
  personsList.Persons.Add(item)
Next

AutoMapperを使ってコレクション丸ごとのデータをコピーする例(上:C#、下:VB)
Mapメソッドの型引数にコピー先のコレクションの型を指定し、引数にコピー元のコレクションオブジェクトを渡す。CreateMapメソッドの呼び出しは前と同じで、コピー元とコピー先のクラスを登録する(コレクションのクラスではない)。
この例では、Mapメソッドから返されたコレクションをforeachループで再び詰め替えており、効率的ではない。Mapメソッドから返されたコレクションをそのままデータコンテキストに設定するような場合には、効率がよい。どちらの方法を使うべきかは、ケースバイケースである。

カテゴリ:オープンソース・ライブラリ 処理対象:データ型
カテゴリ:C# 処理対象:データ型
カテゴリ:Visual Basic 処理対象:データ型


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。