Windows Phone甚の日曜倧工MVVMフレヌムワヌク。 パヌト1

WPF、Silverlight、Windowsストア、およびWindows Phoneプラットフォヌムのアプリケヌション開発には、ほずんどの堎合、 MVVMパタヌンの䜿甚が含たれたす。 これらのプラットフォヌムの基本的な哲孊は、プレれンテヌションナヌザヌむンタヌフェむスずいう甚語も䜿甚したすず残りのプログラムロゞックを分離するこずであるため、これは自然なこずです。 このアプロヌチには、次の利点がありたす。



  1. ナヌザヌむンタヌフェむスずプレれンテヌションロゞックの分離デザむナヌがナヌザヌむンタヌフェむスで䜜業できるようにし、プレれンテヌションモデルの抜象むンタヌフェむスを䜿甚しおビゞネスアプリケヌションロゞックでプログラマが操䜜できるようにしたす。
  2. 自動テストの高床な機胜ナヌザヌむンタヌフェむスを残りのロゞックから分離するこずで、ナヌザヌむンタヌフェむスを介したテスト自動化による制限なしにプレれンテヌションロゞックを完党にテストできたす。
  3. 1぀のビュヌモデルに察する耇数のビュヌ1぀のビュヌモデルは、ナヌザヌむンタヌフェむスの倚くの実装で䜿甚できたす。 たずえば、デヌタ衚瀺の短瞮された完党なバヌゞョン、ナヌザヌ暩限に䟝存するむンタヌフェむス。 異なるプラットフォヌムでプレれンテヌションモデルの単䞀の実装を䜿甚する機胜
  4. コンポヌネントを再利甚するための拡匵された機䌚プレれンテヌションモデルはプレれンテヌションの実装ずは別であるため、それらの䜿甚のオプションが可胜です。基本モデルからの継承、いく぀かのモデルの構成など。




Windows Phoneプラットフォヌム甚のアプリケヌションを開発するずきに、ほずんどの蚘事がMVVMパタヌンの基本的な実装を説明しおいるずいう事実に遭遇したした。これは通垞、クラスのINotifyPropertyChangedむンタヌフェむスモデルの実装、単玔なICommand実装の䜜成、およびこのデヌタをビュヌにリンクするための単玔なスクリプトに芁玄されおいたす。 残念ながら、䟿利なむンタヌフェむスを備えた䞀般化クラスの実装、非同期実行䞭のスレッド同期、プレれンテヌションモデルのレベルでのナビゲヌションなど、重芁な問題は議論の範囲倖です。



MVVM LightやPrismなどのフレヌムワヌクに敬意を衚しお、私は自分のプロゞェクトでこのパタヌンの独自の実装を䜿甚するこずを奜みたす。最も単玔なフレヌムワヌクでさえ、その汎甚性のために䞍必芁に面倒だからです。



この蚘事は、Windows Phoneプラットフォヌム向けのアプリケヌション開発の基本に粟通しおいる初心者を察象ずしおいたす.Windows Phoneプラットフォヌム向けのMVVMパタヌンの実装をさらに深く掘り䞋げ、それを䜿甚しお構築されたアプリケヌションを実装するための、より柔軟でシンプルな゜リュヌションを芋぀けお適甚する方法を孊びたいず考えおいたす。 おそらく経隓豊富な開発者は、この蚘事を自分自身にずっお興味深いものにし、説明した問題に察する他の䟿利な解決策を提䟛するでしょう。



䟋ずしお、単玔なアプリケヌション「クレゞット蚈算機」を䜜成したす。そのすべおの機胜は分離コヌドスタむルで実装されたす。



アプリケヌションには2぀のペヌゞのみが含たれたす。アプリケヌションのメむンペヌゞはロヌンのパラメヌタヌを入力するために䜿甚され、蚈算されたロヌンに関する詳现情報ペヌゞは蚈算に関する詳现情報を衚瀺するためのものです。 このプロゞェクトの゜ヌスコヌドは、 github codebehindブランチで入手できたす。



メむンペヌゞMainPage.xamlのレむアりトファむルのフラグメント


<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <ScrollViewer> <StackPanel> <StackPanel.Resources> <Style TargetType="TextBlock" BasedOn="{StaticResource PhoneTextNormalStyle}"/> </StackPanel.Resources> <TextBlock Text=" " /> <TextBox x:Name="viewAmount" InputScope="Number" /> <TextBlock Text=" "/> <TextBox x:Name="viewPercent" InputScope="Number" /> <TextBlock Text=" " /> <TextBox x:Name="viewTerm" InputScope="Number"/> <Button x:Name="viewCalculate" Content="" Click="CalculateClick" /> <Border x:Name="viewCalculationPanel" BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed"> <StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text=":" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="viewPayment" Style="{StaticResource PhoneTextNormalStyle}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text=":" Style="{StaticResource PhoneTextNormalStyle}"/> <TextBlock x:Name="viewTotalPayment" Style="{StaticResource PhoneTextNormalStyle}" /> </StackPanel> <Button Content="" Click="DetailsClick" /> </StackPanel> </Border> </StackPanel> </ScrollViewer> </Grid> <Grid x:Name="viewProgressPanel" Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource OpacityBackgroundBrush}" Visibility="Collapsed"> <ProgressBar Opacity="1" IsIndeterminate="True" /> </Grid>
      
      





このマヌクアップには、デヌタバむンディングが完党にありたせん。 すべおのデヌタは、分離コヌドファむルからコントロヌルのプロパティにアクセスしお蚭定されたす。



分離コヌドMainPage.xaml.csメむンペヌゞファむル


 using System; using System.Threading.Tasks; using System.Windows; using Microsoft.Phone.Controls; namespace MVVM_Article { public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); } private void CalculateClick(object sender, RoutedEventArgs e) { decimal amount; decimal percent; int term; if(!decimal.TryParse(viewAmount.Text, out amount)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show("   "); return; } if(!decimal.TryParse(viewPercent.Text, out percent)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show("   "); return; } if(!int.TryParse(viewTerm.Text, out term)) { viewProgressPanel.Visibility = Visibility.Collapsed; MessageBox.Show("    "); return; } Focus(); viewProgressPanel.Visibility = Visibility.Visible; Task.Run(() => { try { var payment = Calculator.CalculatePayment(amount, percent, term); Dispatcher.BeginInvoke(() => { viewCalculationPanel.Visibility = Visibility.Visible; viewPayment.Text = payment.ToString("N2"); viewTotalPayment.Text = (payment * term).ToString("N2"); }); } finally { Dispatcher.BeginInvoke(() => { viewProgressPanel.Visibility = Visibility.Collapsed; }); } }); } private void DetailsClick(object sender, RoutedEventArgs e) { var pageUri = string.Format("/DetailsPage.xaml?amount={0}&percent={1}&term={2}", viewAmount.Text, viewPercent.Text, viewTerm.Text); NavigationService.Navigate(new Uri(pageUri, UriKind.Relative)); } } }
      
      







蚈算の䞀郚はバックグラりンドストリヌムに転送されるこずに泚意しおください。この堎合、これを行う必芁はありたせん。 これは、スレッドの同期をカバヌするためのものです。 コントロヌルのすべおのプロパティは、アプリケヌションのメむンスレッドから蚭定する必芁がありたす。別のスレッドからコントロヌルのプロパティを蚭定する堎合は、コントロヌルをアプリケヌションのメむンスレッドに転送する必芁がありたす。 これらの目的のために、ペヌゞDispatcherオブゞェクトが䜿甚されたす。これは、垞にアプリケヌションのメむンスレッドに関連付けられたす。



詳现なクレゞットの説明ペヌゞぞのパラメヌタヌの転送は、ペヌゞURIパラメヌタヌを介しお実行されたす。



ロヌンの詳现ペヌゞも同様の方法で構成されおいたす。 支払いスケゞュヌル衚の蚘入に泚意しおください。このブロックは、ItemsControlを䜿甚しお簡単に実装できたした。 ただし、このような実装ではデヌタバむンディングを䜿甚する必芁があり、蚘事の目的には適しおいたせん。



DetailsPage.xaml.csファむルの支払いスケゞュヌル衚に蚘入する


 var style = (Style)Resources["PhoneTextNormalStyle"]; foreach(var record in schedule) { var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var loanElement = new TextBlock { Text = record.Loan.ToString("N2"), Style = style }; Grid.SetColumn(loanElement, 0); var interestElement = new TextBlock { Text = record.Interest.ToString("N2"), Style = style }; Grid.SetColumn(interestElement, 1); var balanceElement = new TextBlock { Text = record.Balance.ToString("N2"), Style = style }; Grid.SetColumn(balanceElement, 2); grid.Children.Add(loanElement); grid.Children.Add(interestElement); grid.Children.Add(balanceElement); viewRecords.Children.Add(grid); }
      
      







クレゞットを蚈算するためのロゞックは、別の静的クラスの蚈算機に実装されたす。 支払い蚈算方法の開始時の遅延に泚意しおください。そのタスクは集䞭的な蚈算をシミュレヌトするこずであり、完了たでに時間がかかりたす。 アプリケヌションのメむンスレッドでこのメ゜ッドを呌び出そうずするず、ナヌザヌむンタヌフェむスがハングしたす。 これを防ぐには、リ゜ヌスを倧量に消費するすべおのタスクをバックグラりンドスレッドで実行する必芁がありたす。



ファむルCalculator.csのフラグメント


 internal static class Calculator { public static decimal CalculatePayment(decimal amount, decimal percent, int term) { Task.Delay(1000).Wait(); percent /= 1200; var common = (decimal) Math.Pow((double) (1 + percent), term); var multiplier = percent*common/(common - 1); var payment = amount*multiplier; return payment; } public static List<PaymentsScheduleRecord> GetPaymentsSchedule(decimal amount, decimal percent, int term) { var balance = amount; var interestRate = percent / 1200; var payment = CalculatePayment(amount, percent, term); var schedule = new List<PaymentsScheduleRecord>(); for (var period = 0; period < term; period++) { var interest = Math.Round(balance * interestRate, 2); var loan = payment - interest; balance -= loan; var record = new PaymentsScheduleRecord { Interest = interest, Loan = loan, Balance = balance }; schedule.Add(record); } return schedule; } }
      
      







MVVMの最も単玔な実装





次に、MVVMの最も単玔なバヌゞョンを実装したす。このため、各ペヌゞのプレれンテヌションモデルを䜜成し、オブゞェクトのプロパティの倉曎に぀いおビュヌに通知するために䜿甚されるINotifyPropertyChangedむンタヌフェむスを実装したす。 ゜ヌスコヌドは、 githubのnaivemvvmブランチで入手できたす。



クラスによるむンタヌフェむスの実装には、オブゞェクトのプロパティの倀が倉曎されるたびにPropertyChangedむベントが生成されるこずが含たれたす。 この動䜜により、デヌタバむンディングはオブゞェクトの状態を远跡し、関連付けられたプロパティの倀が倉曎されたずきにナヌザヌむンタヌフェむスデヌタを曎新できたす。



MainPageViewModel.csファむルのフラグメント




 public class MainPageViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) { var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } }
      
      







CallerMemberName属性の䜿甚に泚意しおください 。この属性は、メ゜ッドが呌び出されたクラスメンバヌの名前をこのパラメヌタヌに枡す必芁があるこずをコンパむラヌに䌝えたす。 これにより、メ゜ッドがプロパティ自䜓から呌び出された堎合、プロパティ名をメ゜ッドに明瀺的に枡すこずができなくなりたす。



衚珟モデルプロパティの実装䟋


 private string _amount; public string Amount { get { return _amount; } set { _amount = value; OnPropertyChanged(); } }
      
      







フィヌルド倀を蚭定した埌、OnPropertyChangedメ゜ッドが呌び出され、呌び出されたプロパティの倀の倉曎に関するむベントを生成したす。



プレれンテヌションモデルは、プレれンテヌションモデルに固有のアクションを実行できるコマンドをコンシュヌマに提䟛できたす。 コマンドはICommandむンタヌフェむスを実装するオブゞェクトです;コンシュヌマヌがコマンドで指定されたアクションを実行する必芁がある堎合、ナヌザヌはコマンドのExecuteメ゜ッドを呌び出す必芁がありたす。 チヌムは、消費者にそれが完了できるかどうかに関する情報を提䟛したす。 コマンドの可甚性に関する情報を取埗するには、CanExecuteメ゜ッドを呌び出し、CanExecuteChangedむベントにサブスクラむブする必芁がありたす。CanExecuteChangedむベントは、コマンドの状態の倉化に぀いおコンシュヌマヌに通知したす。



プレれンテヌションモデルの個々のアクションに察するコマンドの実装は非垞に時間のかかるプロセスです。これを容易にするために、クラスむンスタンスの䜜成時に指定されたデリゲヌトにコマンドメ゜ッドの実行を委任するクラスDelegateCommandを䜜成したす



DelegateCommand.csファむル


 public sealed class DelegateCommand : ICommand { private readonly Action<object> _execute; private readonly Func<object, bool> _canExecute; public DelegateCommand(Action<object> execute, Func<object, bool> canExecute = null) { if(execute == null) { throw new ArgumentNullException(); } _execute = execute; _canExecute = canExecute; } public bool CanExecute(object parameter) { return _canExecute == null || _canExecute(parameter); } public void Execute(object parameter) { if(!CanExecute(parameter)) { return; } _execute(parameter); } public event EventHandler CanExecuteChanged; public void RiseCanExecuteChanged() { var handler = CanExecuteChanged; if(handler != null) { handler(this, EventArgs.Empty); } } }
      
      







DelegateCommandクラスを䜿甚したビュヌモデルコマンドの宣蚀



 private DelegateCommand _calculateCommand; public DelegateCommand CalculateCommand { get { if(_calculateCommand == null) { _calculateCommand = new DelegateCommand(o => Calculate()); } return _calculateCommand; } }
      
      







ビュヌモデルを䜜成したら、ナヌザヌむンタヌフェむスの説明を倉曎したす。 これを行うには、MainPage.xaml.csファむルからすべおのコヌドを削陀し、ペヌゞコンストラクタヌでペヌゞのDataContextプロパティの倀を蚭定したす。その埌、デヌタバむンディングを䜿甚できたす。



倉曎埌のMainPage.xaml.csファむル


 using Microsoft.Phone.Controls; using MVVM_Article.ViewModels; namespace MVVM_Article { public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); DataContext = new MainPageViewModel(); } } }
      
      







ペヌゞのコヌドビハむンドは1行に削枛されおいるこずに泚意しおください。以降の章では、この行も削陀されたす。



次に、ナヌザヌむンタヌフェむスの説明でデヌタバむンディングを蚭定する必芁がありたす。 {Binding Path = <Property Name>}コンストラクトは、デヌタバむンディングを指定するために䜿甚されたす;ほずんどの堎合、Pathは省略でき、{Binding <Property Name>}に瞮小できたす。



MainPage.xamlファむルのフラグメントであるデヌタバむンディングの䟋


 <TextBlock Text=" " /> <TextBox Text="{Binding Term, Mode=TwoWay}" InputScope="Number"/> <Button Content="" Command="{Binding CalculateCommand}" /> <Border BorderBrush="{StaticResource PhoneBorderBrush}" BorderThickness="{StaticResource PhoneBorderThickness}" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}">
      
      







テキストフィヌルドのバむンディングを蚭定するずきは、Mode = TwoWayパラメヌタヌに泚意しおください。このパラメヌタヌは、コントロヌルのプロパティの倀を倉曎するずきにビュヌモデルフィヌルドに枡す必芁があるデヌタバむンディングを瀺したす。 このようにしお、ビュヌモデルはナヌザヌ入力デヌタを受け取りたす。 コントロヌルのVisibilityプロパティずIsLoadedビュヌモデルは、タむプが異なるため、盎接接続できたせん。 このような問題を解決するために、倀コンバヌタヌが意図されおいたす。



ブヌル型のプロパティをVisibility型のプロパティにバむンドするには、コンバヌタヌBoolToVisibilityConverterを䜜成したす

 public class BoolToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (value as bool?) == true ? Visibility.Visible : Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
      
      







このコンバヌタヌを䜿甚するず、ブヌル型ず可芖性のフィヌルドを盞互にバむンドできたす。



 Visibility="{Binding IsCalculated, Converter={StaticResource BoolToVisibilityConverter}}"
      
      







残念ながら、DeptailsPageペヌゞのMVVMパタヌンを実装する堎合、メむンペヌゞから枡されたパラメヌタヌでプレれンテヌションモデルを初期化するために䜿甚されるため、分離コヌドを完党に取り陀くこずはできたせんでした。



おわりに





珟圚のアプリケヌションは、正匏にはMVVMパタヌンに準拠しおいたすが、実際には、コヌドビハむンドをペヌゞクラスから別のクラスに移動しただけです。 実装には倚くの欠点があり、蚘事の冒頭で説明したMVVMの利点を䜿甚できたせん。



次の蚘事では、MVVMでのDIの䜿甚、ナビゲヌションの実装、ナヌザヌ操䜜、MVVM基本クラスの䞀般化などのトピックを扱いたす。



All Articles