WCF RIA Services と ADO.NET Entity Framework を使った Silverlight の CRUD サンプル

ネタ元→
WCF から得られた Entities に対して LINQ は発行できるでしょうか?

そういえば、Silverlight + WCF RIA Services + ADO.NET Entity Framework を組み合わせたサンプルってあんまり見ないですね。ネタ元のスレッドでも要望されているので、ちょっとこしらえてみました。WCF RIA Services ちゃんと触ったの今回が初めてなんで、Silverlight からサービスを呼び出す部分はあんまり自信ない。

紙面の都合で Visual Studio が生成してくれる Entity Data Model のコードは省略。自分が書いた部分だけを掲載します。作成したプロジェクトは、後で github に公開する予定です。記事の最後に、作成したプロジェクトへのリンクを貼っています。

CustomerDomainService

取得用メソッドはウィザードが生成してくれたので、保存と削除を追加。クライアントから呼び出せるように、Invoke 属性をつけています。

namespace RiaSample.Web
{
    using System;
    using System.Data;
    using System.Linq;
    using System.ServiceModel.DomainServices.EntityFramework;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server;

    /// <summary>
    /// SQL Server の操作に ADO.NET Entity Framework を使う DomainService。
    /// </summary>
    [EnableClientAccess()]
    public class CustomerDomainService : LinqToEntitiesDomainService<TestEntities>
    {
        /// <summary>
        /// 得意先を全件取得します。
        /// </summary>
        /// <returns>全ての得意先データ。</returns>
        public IQueryable<Customer> GetCustomer()
        {
            return this.ObjectContext.Customer;
        }

        // ここから下は追加分。
        // クライアントから呼び出せるように Invoke 属性をつけておく。

        /// <summary>
        /// 得意先を追加または更新します。
        /// </summary>
        /// <param name="customer">追加または更新する得意先。</param>
        [Invoke]
        public void InsertOrUpdate(Customer customer)
        {
            var target = (from c in ObjectContext.Customer
                          where c.Id == customer.Id
                          select c).FirstOrDefault();
            if (null != target)
            {
                // 更新
                target.Name = customer.Name;
                target.Updated = DateTime.Now;
                ObjectContext.SaveChanges();
            }
            else
            {
                // 追加
                customer.Updated = DateTime.Now;
                ObjectContext.Customer.AddObject(customer);
                ObjectContext.SaveChanges();
            }
        }

        /// <summary>
        /// 得意先を削除します。
        /// </summary>
        /// <param name="customer">削除する得意先。</param>
        [Invoke]
        public void DeleteCustomer(Customer customer)
        {
            if (customer.EntityState == EntityState.Detached)
            {
                ObjectContext.Customer.Attach(customer);
            }
            ObjectContext.Customer.DeleteObject(customer);
            ObjectContext.SaveChanges();
        }
    }
}
MainViewModel

サービス呼び出しを Window にべた書きしたくなかったので、MVVM パターンを適用。データの保持と操作、サービスの呼び出しを、ViewModel に隠ぺいしています。

using System;
using System.ComponentModel;
using System.Linq;
using System.ServiceModel.DomainServices.Client;
using System.Windows;
using RiaSample.Web;

namespace RiaSample
{
    public class MainViewModel : INotifyPropertyChanged
    {
        /// <summary>
        /// 表示中のデータ。
        /// </summary>
        public Customer _currentCustomer = new Customer();

        /// <summary>
        /// 表示中のデータを取得または設定します。
        /// </summary>
        public Customer CurrentCustomer
        {
            get { return _currentCustomer; }
            set
            {
                if (_currentCustomer!=value)
                {
                    _currentCustomer = value;
                    OnPropertyChanged("CurrentCustomer");
                }
            }
        }

        /// <summary>
        /// 検索対象データのID
        /// </summary>
        private int _findId = 0;

        /// <summary>
        /// 検索対象データのIDを取得または設定します。
        /// </summary>
        public int FindId
        {
            get { return _findId; }
            set
            {
                if (_findId != value)
                {
                    _findId = value;
                    OnPropertyChanged("FindId");
                }
            }
        }

        /// <summary>
        /// サービスからデータを取得します。
        /// </summary>
        public void Load()
        {
            CustomerDomainContext context = new CustomerDomainContext();
            LoadOperation<Customer> operation = context.Load(context.GetCustomerQuery());
            operation.Completed += (sender, e) =>
            {
                LoadOperation<Customer> op = (LoadOperation<Customer>)sender;
                CurrentCustomer = (from customer in op.Entities
                                   where customer.Id == FindId
                                   select customer).FirstOrDefault();
            };
        }

        /// <summary>
        /// メッセージを表示。
        /// ※単体テストができるように、差し替え可能にしておく。
        /// </summary>
        internal Action<string> ShowMessageBox = message =>
        {
            MessageBox.Show(message);
        };

        /// <summary>
        /// 表示を新しいデータに差し替えます。
        /// </summary>
        public void New()
        {
            CurrentCustomer = new Customer();
        }

        /// <summary>
        /// 編集された内容をサービスに保存します。
        /// </summary>
        public void Save()
        {
            if (null == CurrentCustomer)
            {
                return;
            }
            CustomerDomainContext context = new CustomerDomainContext();
            InvokeOperation operation = context.InsertOrUpdate(CurrentCustomer);
            operation.Completed += (sender, e) =>
            {
                InvokeOperation op = (InvokeOperation)sender;
                if (op.HasError)
                {
                    ShowMessageBox(op.Error.Message);
                }
                else
                {
                    ShowMessageBox("保存に成功しました。");
                }
            };
        }

        /// <summary>
        /// 表示中のデータを削除します。
        /// </summary>
        public void Delete()
        {
            if (null == CurrentCustomer)
            {
                return;
            }
            CustomerDomainContext context = new CustomerDomainContext();
            InvokeOperation operation = context.DeleteCustomer(CurrentCustomer);
            operation.Completed += (sender, e) =>
            {
                InvokeOperation op = (InvokeOperation)sender;
                if (op.HasError)
                {
                    ShowMessageBox(op.Error.Message);
                }
                else
                {
                    ShowMessageBox("削除に成功しました。");
                }
                New();
            };
        }

        /// <summary>
        /// プロパティが変更されたときに発生します。
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// PropertyChanged イベントを発生させます。
        /// </summary>
        /// <param name="propertyName">プロパティ名。</param>
        protected void OnPropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (null != h)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
MainWindow

MainViewModel が持つデータの表示と、MainViewModel のメソッドを呼び出すだけの、簡単なお仕事。

<UserControl x:Class="RiaSample.MainPage"
    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" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="0">
            <TextBlock Text="SearchId"/>
            <TextBox Text="{Binding Path=FindId, Mode=TwoWay}"/>
            <TextBlock Text="Id"/>
            <TextBox Text="{Binding Path=CurrentCustomer.Id}"
                     IsReadOnly="True"/>
            <TextBlock Text="Name"/>
            <TextBox Text="{Binding Path=CurrentCustomer.Name, Mode=TwoWay}"/>
        </StackPanel>
        <StackPanel Grid.Row="1"
                    Orientation="Horizontal">
            <Button Content="New"
                    Click="NewButtonClick"/>
            <Button Content="Load"
                    Click="LoadButtonClick"/>
            <Button Content="Save"
                    Click="SaveButtonClick"/>
            <Button Content="Delete"
                    Click="DeleteButtonClick"/>
        </StackPanel>
    </Grid>
</UserControl>
using System.Windows;
using System.Windows.Controls;

namespace RiaSample
{
    public partial class MainPage : UserControl
    {
        /// <summary>
        /// ビューモデル。サービス呼び出しやデータの保持を隠ぺい。
        /// </summary>
        private readonly MainViewModel _viewModel;

        public MainPage()
        {
            InitializeComponent();

            _viewModel = new MainViewModel();
            DataContext = _viewModel;
        }

        // ビューモデルのメソッドを呼び出すだけ。

        private void NewButtonClick(object sender, RoutedEventArgs e)
        {
            _viewModel.New();
        }

        private void LoadButtonClick(object sender, RoutedEventArgs e)
        {
            _viewModel.Load();
        }

        private void SaveButtonClick(object sender, RoutedEventArgs e)
        {
            _viewModel.Save();
        }

        private void DeleteButtonClick(object sender, RoutedEventArgs e)
        {
            _viewModel.Delete();
        }
    }
}

作成したプロジェクト

下のリンクからダウンロードできます。