web-dev-qa-db-ja.com

WPF MVVM-アプリケーションへの単純なログイン

私はWPFの学習を続けており、現時点ではMVVMに重点を置いており、Karl Shifflettの「MVVM In a Box」チュートリアルを使用しています。ただし、ビュー/ビューモデル間でのデータの共有と、画面上のビューを更新する方法について質問があります。 PS IOCについてはまだ取り上げていません。

以下は、テストアプリケーションでのメインウィンドウのスクリーンショットです。その3つのセクション(ビュー)、ヘッダー、ボタン付きのスライドパネル、およびアプリケーションのメインビューとしての残りに分割されています。アプリケーションの目的は簡単で、アプリケーションにログインします。ログインが成功すると、ログインビューは新しいビュー(OverviewScreenViewなど)に置き換えられて消え、アプリケーションのスライドの関連ボタンが表示されます。

Main Window

アプリケーションが2つのViewModelを持っていると思います。 1つはMainWindowView用で、もう1つはLoginView用です。MainWindowはログイン用のコマンドを必要としないため、個別に保持しました。

IOCについてはまだ取り上げていないため、シングルトンであるLoginModelクラスを作成しました。 「public bool LoggedIn」である1つのプロパティと、UserLoggedInと呼ばれるイベントのみが含まれます。

MainWindowViewModelコンストラクターは、イベントUserLoggedInに登録します。 LoginViewで、ユーザーがLoginViewで[ログイン]をクリックすると、LoginViewModelでコマンドが呼び出され、ユーザー名とパスワードが正しく入力されると、LoginModelが呼び出され、LoggedInがtrueに設定されます。これにより、UserLoggedInイベントが発生します。これはMainWindowViewModelで処理され、ビューがLoginViewを非表示にして、別のビュー、つまり概要画面に置き換えます。

質問

Q1。明らかな質問ですが、MVVMを正しく使用するには、このようにログインする必要があります。つまり、制御の流れは次のとおりです。 LoginView-> LoginViewViewModel-> LoginModel-> MainWindowViewModel-> MainWindowView。

Q2。ユーザーがログインし、MainWindowViewModelがイベントを処理したと仮定します。新しいビューを作成して、LoginViewが置かれていた場所に配置するにはどうすればよいでしょうか。同様に、不要になったLoginViewを破棄するにはどうすればよいでしょうか。 LoginViewまたはOverviewScreenViewに設定される「UserControl currentControl」のようなMainWindowViewModelにプロパティがあるでしょうか。

Q3。 MainWindowのビジュアルスタジオデザイナーでLoginViewを設定する必要があります。または、空白のままにしておくと、プログラムで誰もログインしていないことがわかるので、MainWindowが読み込まれると、LoginViewが作成され、画面に表示されます。

質問への回答に役立つ場合は、以下のコードサンプル

MainWindowのXAML

<Window x:Class="WpfApplication1.MainWindow"
    xmlns:local="clr-namespace:WpfApplication1"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="372" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <local:HeaderView Grid.ColumnSpan="2" />

        <local:ButtonsView Grid.Row="1" Margin="6,6,3,6" />

        <local:LoginView Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</Window>

MainWindowViewModel

using System;
using System.Windows.Controls;
using WpfApplication1.Infrastructure;

namespace WpfApplication1
{
    public class MainWindowViewModel : ObservableObject
    {
        LoginModel _loginModel = LoginModel.GetInstance();
        private UserControl _currentControl;

        public MainWindowViewModel()
        {
            _loginModel.UserLoggedIn += _loginModel_UserLoggedIn;
            _loginModel.UserLoggedOut += _loginModel_UserLoggedOut;
        }

        void _loginModel_UserLoggedOut(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }

        void _loginModel_UserLoggedIn(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }
    }
}

LoginViewViewModel

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Input;
using WpfApplication1.Infrastructure;

namespace WpfApplication1
{
    public class LoginViewViewModel : ObservableObject
    {
        #region Properties
        private string _username;
        public string Username
        {
            get { return _username; }
            set
            {
                _username = value;
                RaisePropertyChanged("Username");
            }
        }
        #endregion

        #region Commands

        public ICommand LoginCommand
        {
            get { return new RelayCommand<PasswordBox>(LoginExecute, pb => CanLoginExecute()); }
        }

        #endregion //Commands

        #region Command Methods
        Boolean CanLoginExecute()
        {
            return !string.IsNullOrEmpty(_username);
        }

        void LoginExecute(PasswordBox passwordBox)
        {
            string value = passwordBox.Password;
            if (!CanLoginExecute()) return;

            if (_username == "username" && value == "password")
            {
                LoginModel.GetInstance().LoggedIn = true;
            }
        }
        #endregion
    }
}
22
JonWillis

聖なる質問、バットマン!

Q1:プロセスは機能しますが、LoginModelを使用してMainWindowViewModelと通信することについては知りません。

_LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView_のようなものを試すことができます

シングルトンはアンチパターンと見なされている人もいることは知っていますが、このような状況ではこれが最も簡単だと思います。このように、シングルトンクラスはINotifyPropertyChangedインターフェイスを実装し、login\outイベントが検出されるたびにイベントを発生させることができます。

LoginCommandLoginViewModelまたはシングルトンに実装します(個人的には、これをViewModelに実装して、ViewModelと「back-終了」ユーティリティクラス)。このログインコマンドは、シングルトンのメソッドを呼び出してログインを実行します。

Q2:これらの場合、通常、PageManagerまたはViewModelManagerとして機能する(まだ別の)シングルトンクラスがあります。このクラスは、トップレベルページまたはCurrentPage(単一ページのみの状況)への参照を作成、破棄、および保持します。

私のViewModelBaseクラスには、クラスを表示しているUserControlの現在のインスタンスを保持するプロパティもあります。これにより、LoadedイベントとUnloadedイベントをフックできます。これにより、ViewModelで定義できる仮想OnLoaded(), OnDisplayed() and OnClosed()メソッドを使用できるようになるため、ページでロードおよびアンロードアクションを実行できます。

MainWindowViewが_ViewModelManager.CurrentPage_インスタンスを表示しているときに、このインスタンスが変更されると、Unloadedイベントが発生し、私のページのDisposeメソッドが呼び出され、最終的にGCが入り、残りは整頓されます。

Q3:これを理解しているかどうかはわかりませんが、「ユーザーがログインしていないときにログインページを表示する」という意味だといいのですが、その場合はViewModelToViewConverterは、ユーザーがログインしていないときに(SecurityContextシングルトンをチェックして)指示を無視し、代わりにLoginViewテンプレートのみを表示します。これは、特定のユーザーだけがアクセス権を持つページが必要な場合にも役立ちます。ビューを作成してセキュリティプロンプトに置き換える前に、セキュリティ要件を確認できる場所を確認または使用します。

長い答えて申し訳ありませんが、これが役に立てば幸いです:)

編集:また、「管理」のスペルを間違えました


コメント内の質問を編集

LoginManagerSingletonはどのようにMainWindowViewと直接通信しますか。 MainWindowViewの背後にコードがないように、すべてがMainWindowViewModelを通過する必要はありません。

申し訳ありませんが、明確にするために-LoginManagerがMainWindowViewと直接対話する(これは単なるビューであるため)のではなく、LoginManagerが次の呼び出しに応答してCurrentUserプロパティを設定するだけです。 LoginCommandが作成すると、PropertyChangedイベントが発生し、MainWindowView(変更をリッスンしている)がそれに応じて反応します。

次に、LoginManagerはPageManager.Open(new OverviewScreen())(またはIOCが実装されている場合はPageManager.Open("overview.screen"))を呼び出して、たとえば、ユーザーがログに記録されたときに表示されるデフォルト画面にユーザーをリダイレクトできます。に。

LoginManagerは基本的に実際のログインプロセスの最後のステップであり、ビューはこれを適切に反映するだけです。

また、これを入力するときに、LoginManagerシングルトンを使用するのではなく、PageManagerクラスにすべて格納できることに気付きました。ログインに成功したときにCurrentUserを設定するLogin(string, string)メソッドを用意するだけです。

私は基本的にPageManagerViewModelを通じてPageManagerViewのアイデアを理解しています

私はPageManagerをView-ViewModelデザインにするように設計しません。INotifyPropertyChangedを実装する通常の世帯シングルトンがうまくいくはずです。これにより、MainWindowViewがCurrentPageプロパティの変更に反応できるようになります。

ViewModelBaseはあなたが作成した抽象クラスですか?

はい。このクラスをすべてのViewModelの基本クラスとして使用します。

このクラスには

  • Title、PageKey、OverriddenUserContextなど、すべてのページで使用されるプロパティ。
  • PageLoaded、PageDisplayed、PageSaved、PageClosedなどの一般的な仮想メソッド
  • INPCを実装し、PropertyChangedイベントを発生させるために使用する保護されたOnPropertyChangedメソッドを公開します。
  • また、ClosePageCommand、SavePageCommandなど、ページと対話するためのスケルトンコマンドを提供します。

ログインが検出されると、CurrentControlは新しいビューに設定されます

個人的には、現在表示されているViewModelBaseのインスタンスのみを保持します。次に、これはContentControlのMainWindowViewによって_Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}"_のように参照されます。

次に、コンバーターを使用してViewModelBaseインスタンスをUserControlに変換しますが、これは完全にオプションです。 ResourceDictionaryエントリに依存することもできますが、このメソッドを使用すると、開発者は呼び出しを傍受し、必要に応じてSecurityPageまたはErrorPageを表示することもできます。

次に、アプリケーションが起動すると、誰もログインしていないことを検出し、LoginViewを作成して、それをCurrentControlに設定します。 LoginViewがデフォルトで表示されるようにするのではなく、

ユーザーに表示される最初のページがOverviewScreenのインスタンスになるようにアプリケーションを設計できます。これは、PageManagerが現在nullのCurrentUserプロパティを持っているため、ViewModelToViewConverterがこれをインターセプトし、OverviewScreenView UserControlを表示するのではなく、代わりにLoginView UserControlを表示します。

ユーザーが正常にログインすると、LoginViewModelはPageManagerに元のOverviewScreenインスタンスにリダイレクトするように指示しますが、今回はCurrentUserプロパティがnullでないため正しく表示されます。

他の人がそうであるように、人々がこの制限を回避するにはどうすればよいですか、シングルトンは悪いです

私はこれであなたと一緒にいます、私は私が良いシングルトンを好きです。ただし、これらの使用は、必要な場合にのみ使用するように制限する必要があります。しかし、彼らは私の意見では完全に有効な用途を持っていますが、他の誰かがこの問題に参加したいのかどうかわかりませんか?


編集2:

MVVMの公開されているフレームワーク/クラスセットを使用していますか

いいえ、私は過去12か月ほどの間に作成および改良したフレームワークを使用しています。フレームワークはまだmostMVVMガイドラインに準拠していますが、作成する必要のあるコード全体の量を削減するいくつかの個人的なタッチが含まれています。

たとえば、MVVMのいくつかの例では、あなたが持っているのと同じようにビューを設定しています。一方、ビューは、ViewObject.DataContextプロパティ内にViewModelの新しいインスタンスを作成します。これは一部ではうまくいくかもしれませんが、開発者がOnPageLoad()などのViewModelから特定のWindowsイベントをフックすることを許可していません。

私の場合、OnPageLoad()は、ページ上のすべてのコントロールが作成され、画面に表示された後に呼び出されます。これは、コンストラクターが呼び出されてから数分以内に即座に、またはまったく行われない場合があります。たとえば、ページに現在選択されていないタブ内に複数の子ページがある場合に、ページのロードプロセスを高速化するために、ほとんどのデータロードをここで行います。

それだけでなく、この方法でViewModelを作成することにより、各ビューのコード量が少なくとも3行増えます。これはあまり聞こえないかもしれませんが、これらのコード行が重複コードを作成するallビューで基本的に同じであるだけでなく、多くの数を必要とするアプリケーションがある場合、余分な行数がすぐに追加されますビュー。それと、私は本当に怠惰です。私はコードを入力する開発者にはなりませんでした。

ページマネージャーのアイデアを通じて今後の改訂で私が行うことは、タブマネージャーのように一度に複数のビューを開いて、単一のuserControlではなくページマネージャーを制御することです。次に、ページマネージャにバインドされた別のビューでタブを選択できます

この場合、PageManagerは、開いている各ViewModelBaseクラスへの直接参照を保持する必要はなく、最上位のクラスのみを保持します。他のすべてのページは親の子になり、階層をより細かく制御できるようになり、SaveイベントとCloseイベントを細かく制御できるようになります。

これらをPageManagerの_ObservableCollection<ViewModelBase>_プロパティに配置する場合は、MainWindowのTabControlを作成して、ItemsSourceプロパティがPageManagerのChildrenプロパティをポイントし、残りをWPFエンジンに実行させる必要があります。

ViewModelConverterでもう少し拡張できますか

もちろん、概要を説明するために、いくつかのコードを表示する方が簡単でしょう。

_    public override object Convert(object value, SimpleConverterArguments args)
    {
        if (value == null)
            return null;

        ViewModelBase vm = value as ViewModelBase;

        if (vm != null && vm.PageTemplate != null)
            return vm.PageTemplate;

        System.Windows.Controls.UserControl template = GetTemplateFromObject(value);

        if (vm != null)
            vm.PageTemplate = template;

        if (template != null)
            template.DataContext = value;

        return template;
    }
_

このコードをセクションで読むと、次のようになります。

  • 値がnullの場合、戻ります。単純なnull参照チェック。
  • 値がViewModelBaseであり、そのページがすでにロードされている場合は、そのビューを返すだけです。これを行わないと、ページが表示されるたびに新しいビューが作成され、予期しない動作が発生します。
  • ページテンプレートUserControlを取得します(以下を参照)。
  • PageTemplateプロパティを設定して、このインスタンスをフックできるようにし、各パスで新しいインスタンスをロードしないようにします。
  • View DataContextをViewModelインスタンスに設定します。これらの2行は、この時点からeveryビューから先に説明した3行を完全に置き換えます。
  • テンプレートを返します。これは、ユーザーが見られるようにContentPresenterに表示されます。

    _public static System.Windows.Controls.UserControl GetTemplateFromObject(object o)
    {
        System.Windows.Controls.UserControl template = null;
    
        try
        {
            ViewModelBase vm = o as ViewModelBase;
    
            if (vm != null && !vm.CanUserLoad())
                return new View.Core.SystemPages.SecurityPrompt(o);
    
            Type t = convertViewModelTypeToViewType(o.GetType());
    
            if (t != null)
                template = Activator.CreateInstance(t) as System.Windows.Controls.UserControl;
    
            if (template == null)
            {
                if (o is SearchablePage)
                    template = new View.Core.Pages.Generated.ViewList();
                else if (o is MaintenancePage)
                    template = new View.Core.Pages.Generated.MaintenancePage(((MaintenancePage)o).EditingObject);
            }
    
            if (template == null)
                throw new InvalidOperationException(string.Format("Could not generate PageTemplate object for '{0}'", vm != null && !string.IsNullOrEmpty(vm.PageKey) ? vm.PageKey : o.GetType().FullName));
        }
        catch (Exception ex)
        {
            BugReporter.ReportBug(ex);
            template = new View.Core.SystemPages.ErrorPage(ex);
        }
    
        return template;
    }
    _

これは、ほとんどの面倒な作業を行うコンバーターのコードであり、次のセクションを読んでください。

  • メインのtry..catchブロックは、を含むクラス構築エラーをキャッチするために使用されます
    • ページは存在しません、
    • コンストラクターコードの実行時エラー
    • XAMLの致命的なエラー。
  • convertViewModelTypeToViewType()は、ViewModelに対応するViewを見つけようとし、それが本来あるべきタイプのコードを返します(これはnullの場合があります)。
  • これがnullでない場合は、タイプの新しいインスタンスを作成します。
  • 使用するビューが見つからない場合は、そのViewModelタイプのデフォルトページを作成してみてください。私は、ViewModelBaseから継承するいくつかの追加のViewModel基本クラスを持っています。これらは、ページのタイプ間の義務の分離を提供します。
    • たとえば、SearchablePageクラスは、特定のタイプのシステム内のすべてのオブジェクトのリストを表示し、追加、編集、更新、およびフィルターコマンドを提供するだけです。
    • MaintenancePageはデータベースから完全なオブジェクトを取得し、オブジェクトが公開するフィールドのコントロールを動的に生成および配置し、オブジェクトが持つ任意のコレクションに基づいて子ページを作成し、使用するSaveおよびDeleteコマンドを提供します。
  • それでも使用するテンプレートがない場合は、エラーをスローして、開発者が何かがうまくいかないことを知らせます。
  • Catchブロックでは、発生した実行時エラーがわかりやすいErrorPageでユーザーに表示されます。

これにより、ViewModelの開発者が明示的にオーバーライドしていない限り、アプリケーションはデフォルトページを単純に表示するため、ViewModelクラスの作成のみに集中できます。

28
fatty