かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MVVM + Silverlight + WCF RIA Services + Prismでログインして画面遷移

先日書いたのをちょろっと修正しました。
MVVMRxSample.V2.zip

ちょっとこのサンプルで個人的にポイントだと思ってる箇所をつらつらと書いていこうと思います。

認証

Form認証使って認証しています。

サーバー側

まずForm認証使ってます。なのでWebアプリケーション側のWeb.configに下記のようにauthenticationタグを追加しています。

<authentication mode="Forms" />

そしてWCF RIA Servicesの認証サービスを作成して下記のようにカスタム認証ロジックを入れてます。今回はユーザ名とパスワードがuser001の場合OKということにしてます。そして、認証後のユーザに独自のプロパティも追加しています。ProfileProviderがめんどくさかったのでProfileProviderを使わないように属性指定しています。

namespace Okazuki.MVVMRxSample.Web
{
    using System.Security.Principal;
    using System.ServiceModel.DomainServices.Hosting;
    using System.ServiceModel.DomainServices.Server.ApplicationServices;
    using System.Threading;

    [EnableClientAccess]
    public class AuthenticationDomainService : AuthenticationBase<User>
    {
        protected override bool ValidateUser(string userName, string password)
        {
            // ログインに時間がかかるわ
            Thread.Sleep(1000);
            return userName == "user001" && password == "user001";
        }

        protected override User GetAuthenticatedUser(IPrincipal principal)
        {
            var user = base.GetAuthenticatedUser(principal);
            user.UserName = "山田 太郎";
            user.UserDepartment = "無職";
            return user;
        }
    }

    public class User : UserBase
    {
        [ProfileUsage(IsExcluded = true)]
        public string UserName { get; set; }

        [ProfileUsage(IsExcluded = true)]
        public string UserDepartment { get; set; }
    }
}
クライアントサイド

Silverlight側では、WebContext(WCF RIA Servicesが自動生成してくれるクラス)の認証にForm認証を使うように設定する必要があります。これはAppクラスのコンストラクタで指定しています。

// 初期化
var webContext = new WebContext();
webContext.Authentication = new FormsAuthentication();
this.ApplicationLifetimeObjects.Add(webContext);
MVVMRxSampleApplication.Initialize(webContext);

そして、WCF RIA Servicesを使う時のめんどくさいコールバックの処理をReactive Extensionsを使うようにするためにWebContextクラスに拡張メソッドを定義しています。

/// <summary>
/// WebContextのログインを行う。
/// </summary>
/// <param name="self"></param>
/// <param name="userName">ユーザ名</param>
/// <param name="password">パスワード</param>
/// <returns>ユーザ情報。ログインできていない場合はnull。</returns>
public static IObservable<User> LoginAsObservable(this WebContext self, string userName, string password)
{
    // Subscribeまで処理を遅延させるためDeferでくるむ
    return Observable.Defer(() =>
    {
        var async = new AsyncSubject<User>();
        // ログインを行う
        var op = self.Authentication.Login(userName, password);
        // Completedイベントを購読して
        Observable.FromEvent<EventHandler, EventArgs>(
            h => (s, e) => h(e),
            h => op.Completed += h,
            h => op.Completed -= h)
            // 1回のイベント発火でイベントハンドラを解除する
            .Take(1)
            .Subscribe(_ =>
            {
                // キャンセルされてたら何もせず終了
                if (op.IsCanceled)
                {
                    async.OnCompleted();
                    return;
                }

                // エラーの場合はエラー通知
                if (op.HasError)
                {
                    op.MarkErrorAsHandled();
                    async.OnError(op.Error);
                    return;
                }

                // ユーザ情報を発行して終了。
                async.OnNext(op.User as User);
                async.OnCompleted();
            });
        return async.AsObservable();
    });
}

MVVM的なところ

今回は、Modelに割と処理を押し込もうと頑張ってみました。まぁログインしかしてないですが。

Model関連のクラス

いいのか悪いのかわかりませんが、Modelのルート的な存在のクラスを作って、そいつには割り切ってViewModelからグローバルにアクセス出来るようにしました。以下のようなノリで。

MVVMRxSampleApplication.Current.Authentication.LoginUser;

CurrentはMVVMRxSampleApplicationのInitializeメソッドで設定するようにしてます。Appクラスで最初に初期化してくれたらあとはCurrentプロパティで、がんがんアクセスしちゃってくださいって感じですね。あまり好きではないですが、特にいい方法も思いつかないところです。DIコンテナ使うなら、DIコンテナにModelのクラスの管理はまかせちゃいたいなぁと思ったり思わなかったり。

画面遷移

今回は、SL + MVVMで画面遷移するということで、そこらへんの仕組みを作りこんでます。VMからVへの画面遷移の以来はPrismを使ってるのでInteractionRequestを使うのでVM側のコードはこんな感じになってます。

this.NavigateRequest.Raise(
    new Notification { Content = "/A" });

画面遷移専用のNotificationを継承したクラスをつくろうかと思いましたが、惰性でNotificationのContentプロパティに遷移先のURLを入れるようにしました。このNavigateRequestプロパティはViewModelの基本クラスに仕込んでいます。画面遷移が必要じゃないViewModelもいることを考えると、ViewModelの基本クラスはシンプルにしておいてPageViewModelBaseという感じで必要な奴だけNavigateRequestとかを持つようにしたほうが綺麗かなと思いました。

画面遷移の要求を受け取るActionはこんな感じに作ってます。

namespace Okazuki.MVVMRxSample.Behaviors
{
    using System;
    using System.Windows.Controls;
    using System.Windows.Interactivity;
    using Microsoft.Practices.Prism.Interactivity.InteractionRequest;

    /// <summary>
    /// InteractionRequestからの通知を受け取ってFrameにナビゲーションを行うアクション
    /// </summary>
    public class NavigateAction : TriggerAction<Frame>
    {
        protected override void Invoke(object parameter)
        {
            var e = parameter as InteractionRequestedEventArgs;
            if (e == null)
            {
                return;
            }

            var uri = e.Context.Content as string;
            if (uri == null)
            {
                return;
            }

            this.AssociatedObject.Navigate(new Uri(uri, UriKind.Relative));
        }
    }
}

Frameに直接割り当てるActionとして仕立てました。FrameからFrame内のページのViewModelのNavigateRequestに対してInteractionRequestTriggerのSourceObjectプロパティをバインドしないといけないので、MainPage.xamlに以下のような長いBindingのPathが書いてあります。

<sdk:Frame x:Name="frame" 
            IsEnabled="{Binding ElementName=frame, Path=Content.DataContext.IsNotBusy}" 
            JournalOwnership="OwnsJournal">
    <i:Interaction.Triggers>
        <my:InteractionRequestTrigger SourceObject="{Binding Path=Content.DataContext.NavigateRequest, ElementName=frame, UpdateSourceTrigger=PropertyChanged}">
            <my1:NavigateAction />
        </my:InteractionRequestTrigger>
    </i:Interaction.Triggers>
    ...中略...
</sdk:Frame>

処理中の表現

Silverlightは、非同期APIが基本なので非同期でサーバと通信してる最中はユーザに画面触ってほしくない時も大いにあります。ということでViewModelにIsBusyというbool型のプロパティを定義して、処理の前にIsBusyにtrueをセットして処理が終わったらIsBusyをfalseにするという感じで処理中を表現するようにしました。このIsBusyプロパティをBusyIndicatorコントロールのIsBusyプロパティとバインドして処理中の画面を表現しています。

この処理も毎回書くのがめんどくさいのと、非同期処理はIObservableにまとめるので、こんな拡張メソッドを作ってみました。

public static class ViewModelExtensions
{
    public static IObservable<T> BusyProcess<T>(this IObservable<T> self, ViewModelBase viewModel)
    {
        return Observable.Defer<T>(() =>
        {
            viewModel.IsBusy = true;
            return self.Finally(() => viewModel.IsBusy = false);
        });
    }
}

これで、SubscribeされたらIsBusyをtrueにして最後にfalseに戻すという感じです。使う所はこんな感じ。

// Modelにログイン処理を依頼
MVVMRxSampleApplication.Current.Authentication.Login(this.UserName, this.Password)
    .BusyProcess(this)
    .Subscribe(success =>
    {
        if (success)
        {
            // 成功時にはAView.xamlへ遷移
            this.NavigateRequest.Raise(
                new Notification { Content = "/A" });
            return;
        }

        // 失敗を通知
        this.AlertRequest.Raise(
            new Notification
            {
                Title = "ログイン失敗",
                Content = "ユーザ名またはパスワードが違います"
            });
    },
    ex =>
    {
        // エラーを通知
        this.AlertRequest.Raise(
            new Notification
            {
                Title = "エラー",
                Content = "ログイン中にエラーが発生しました"
            });
    });

OnErrorをちゃんと処理しないとFinallyメソッドの処理が実行されないという動きをするので使う側で気を付けないといけないのがイマイチな感じがしますが、まぁこんなもんかなと思います。