添付プロパティと添付ビヘイビア
添付プロパティについて調べてみた。
参考にしたサイトはこれ。
YKSoftware - for WPF Developers
非常に分かりやすかった。
ほぼそのままお借りしてコーディングしてみます。
まずは添付プロパティのクラス。
using System; using System.Windows; using System.Windows.Controls; namespace AttachedPropertySample.AttachedProperty { public class ButtonAtt { public static bool GetIsFileRefButton(DependencyObject obj) { return (bool)obj.GetValue(IsFileRefButtonProperty); } public static void SetIsFileRefButton(DependencyObject obj, bool value) { obj.SetValue(IsFileRefButtonProperty, value); } // Using a DependencyProperty as the backing store for IsFileRefButton. This enables animation, styling, binding, etc... public static readonly DependencyProperty IsFileRefButtonProperty = DependencyProperty.RegisterAttached("IsFileRefButton", typeof(bool), typeof(ButtonAtt), new PropertyMetadata(false, OnIsFileRefChanged)); private static void OnIsFileRefChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var b = d as Button; if(b != null) { if(GetIsFileRefButton(b)) { b.Click += OnClick; } else { b.Click -= OnClick; } } } private static void OnClick(object sender, RoutedEventArgs e) { var b = sender as Button; if (b != null) { var dlg = new Microsoft.Win32.OpenFileDialog(); var result = dlg.ShowDialog(); if (result != null && result == true) { var filename = dlg.FileName; GetCallback(b)?.Invoke(filename); } } } //callback添付プロパティ public static Action<string> GetCallback(DependencyObject obj) { return (Action<string>)obj.GetValue(CallbackProperty); } public static void SetCallback(DependencyObject obj, Action<string> value) { obj.SetValue(CallbackProperty, value); } // Using a DependencyProperty as the backing store for Callback. This enables animation, styling, binding, etc... public static readonly DependencyProperty CallbackProperty = DependencyProperty.RegisterAttached("Callback", typeof(Action<string>), typeof(ButtonAtt), new PropertyMetadata(null)); #endregion } }
次に、View。
<Window x:Class="AttachedPropertySample.View.SubView" 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" xmlns:ignore="http://www.galasoft.ch/ignore" mc:Ignorable="d ignore" xmlns:att="clr-namespace:AttachedPropertySample.AttachedProperty" DataContext="{Binding Sub, Source={StaticResource Locator}}"> <Grid> <StackPanel Orientation="Vertical"> <Button x:Name="button1" Content="Button 1"/> <Button Content="Button 2" /> <Button Content="Button 3" att:ButtonAtt.IsFileRefButton="True" att:ButtonAtt.Callback="{Binding ReadFileCallback}"/> <TextBox Text="{Binding SubText}" /> </StackPanel> </Grid> </Window>
最後にViewModel。
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using AttachedPropertySample.View; using System; using System.Windows.Controls; namespace AttachedPropertySample.ViewModel { /// <summary> /// This class contains properties that a View can data bind to. /// <para> /// See http://www.galasoft.ch/mvvm /// </para> /// </summary> public class SubViewModel : ViewModelBase { private string _subText; public string SubText { get { return _subText; } set { _subText = value; RaisePropertyChanged("SubText"); } } /// <summary> /// Initializes a new instance of the SubWindowViewModel class. /// </summary> public SubViewModel() { } public Action<string> ReadFileCallback { get { return OnReadFile; } } private void OnReadFile(string obj) { SubText = obj; } } }
IEnumerableとIEnumerable<T>
他の人のソースを見ていて気になった。
とある自作クラスでIEnumerable<T>を継承していたが、
本当に継承する必要があるのか、と。
そのときは結果としては継承する必要が無かったわけだが、
自分自身がIEnumerable<T>をよく理解していないため、気付くのが遅かった。
というわけで、今回はIEnumerable<T>について書いてみる。
IEnumerable<T>を継承する意味とは、foreachで要素にアクセスできるようになったり、
LINQが使えるようになること。
作ったサンプルは以下。
public class MyArray<T> : IEnumerable<T> { private T[] array = new T[0]; #region コンストラクタ public MyArray() { } public MyArray(int count) { array = new T[count]; } #endregion #region プロパティ public T this[int index] { get { return array[index]; } set { array[index] = value; } } #endregion public void Add(T item) { T[] temp = new T[array.Length + 1]; array.CopyTo(temp, 0); temp[temp.Length - 1] = item; array = temp; } #region IEnumerable<T>の実装 public IEnumerator<T> GetEnumerator() { foreach(var item in array) { yield return item; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion }
GetEnumerator()とIEnumerable.GetEnumerator()を実装する必要がある。
この辺の書き方はほぼ常套句かな。
で、呼び出し側はこんなコード。
static void Main(string[] args) { //10個の要素でインスタンス生成 MyArray<int> array = new MyArray<int>(10); //追加 array.Add(1); array.Add(4); array.Add(8); foreach (int i in array) { Console.WriteLine(i); } //要素へアクセス Console.WriteLine($"11番目の値は{array[11]}"); //LINQ Console.WriteLine($"Count = {array.Count()}"); Console.WriteLine($"Max = {array.Max()}"); Console.ReadKey(); }
参考にさせていただいたブログは以下。
ありがとうございました。
非同期処理のいろいろ
初心者プログラマーにとって、
非同期処理はとてもハードルが高い。
方法が一つだけならまだしも、
いろいろな手法があって自分の中でなかなか整理できない。
ということで実際にコードを書いてまとめてみた。
参考にした本は、これ。
gihyo.jp
参考書の中ではWindowsフォーム用のコードが書かれていたので、
WPF用に少し修正した。
まず、Threadクラスを使用した非同期処理。
private string _labelText; public string LabelText { get { return _labelText; } set { Set(ref _labelText, value, "LabelText"); } } private RelayCommand _buttonClickCommand; public RelayCommand ButtonClickCommand { get { return _buttonClickCommand ?? (_buttonClickCommand = new RelayCommand(OnButtonClick)); } } public void OnButtonClick() { LabelText = "開始"; //Threadクラスを使用した非同期処理 var th = new Thread(DoLongSomething); th.Start(); } private void DoLongSomething() { Thread.Sleep(5000); LabelText = "終了"; }
簡単に説明をすると、View側にボタンとラベルを配置して、
ボタン開始時にラベルのテキストを変更し、
5秒待機した後、再度ラベルのテキストを変更するというもの。
Viewには別のコントロールも配置していて、
5秒待機している間、UIがフリーズすることなくコントロールを操作できるかを確認している。
この全体の動作はこのあとに登場するコードでも同様。
続いて、Taskクラスを使用した非同期処理。
public void OnButtonClick() { LabelText = "開始"; //Taskクラスを使用した非同期処理 Task.Run(() => DoLongSomething()); } private void DoLongSomething() { Thread.Sleep(5000); LabelText = "終了"; }
次に、await/asyncを使用し、戻り値がないメソッドを呼ぶ。
ポイントは、OnButtonClickメソッドにsysnc修飾子をつけること。
asyncをつけることでawaitが使用可能になり、
awaitをつけたTaskはそのタスクが完了するまで待機する。
public async void OnButtonClick() { LabelText = "開始"; //awaitを使用した非同期処理 await Task.Run(() => DoLongSomething()); //書き方違うバージョン await Task.Run(() => { DoLongSomething(); }); //分割して書くと以下 Task task = Task.Run(() => DoLongSomething()); await task; LabelText = "終了"; } private void DoLongSomething() { Thread.Sleep(5000); }
await/asyncを使用し、戻り値があるメソッドを呼ぶ。
public async void OnButtonClick() { LabelText = "開始"; //戻り値のあるメソッドを非同期処理 var elapsed = await Task.Run(() => DoLongSomething()); LabelText = $"{elapsed}ミリ秒"; } /// <summary> /// 戻り値のある同期メソッド /// </summary> /// <returns></returns> private long DoLongSomething() { var sw = Stopwatch.StartNew(); Thread.Sleep(5000); sw.Stop(); return sw.ElapsedMilliseconds; } 今度は、メソッド自体を非同期メソッドに。戻り値がないパターン。 ポイントは、DoLongSomethingをDoLongSomethingAsyncにして、 async修飾子をつけ、戻りの型をTaskにする。 >|cs| public async void OnButtonClick() { LabelText = "開始"; //戻り値の無い非同期処理メソッドを呼ぶ await DoLongSomethingAsync(); LabelText = "終了"; } /// <summary> /// 戻り値の無い非同期メソッド /// </summary> /// <returns></returns> private async Task DoLongSomethingAsync() { await Task.Run(() => Thread.Sleep(5000)); }
戻り値がある非同期メソッド。
DoLongSomethingAsyncの戻りの型はTask
public async void OnButtonClick() { LabelText = "開始"; //戻り値のある非同期処理メソッドを呼ぶ var elapsed = await DoLongSomethingAsync(); LabelText = $"{elapsed}ミリ秒"; } /// <summary> /// 戻り値のある非同期メソッド /// </summary> /// <returns></returns> private async Task<long> DoLongSomethingAsync() { var sw = Stopwatch.StartNew(); await Task.Run(() => Thread.Sleep(5000)); sw.Stop(); return sw.ElapsedMilliseconds; }
プロジェクトテンプレートの作成
事情により1週間空いてしまったが、今日から再開。
前回はmvvmlightのテンプレートから作成したプロジェクトをいろいろいじって、
別Windowが表示できるところまで進めた。
今後、いろいろなコードを書いていくにあたって、
この状態を基本のプロジェクトとしたいため、プロジェクトテンプレートを作成する。
方法はここに書いてあるため、そのとおりに実施する。
方法 : プロジェクト テンプレートを作成する
まずは、ソリューション名に該当する場所を
パラメータに置き換える。
これはテキストエディタ等で一括置換したほうが早い。
自分が作ったプロジェクト名は「MvvmLightIntroduction」なので、
この文字列を$safeprojectname$に置換する。
ソリューションファイルは置換しちゃだめ。
なお、置換前にプロジェクトフォルダにあるbinとobjフォルダは削除しておいたほうがいい。
Visual Studioでプロジェクトを開いた状態で実施すると、
再読み込みするか聞いてくるので、すべて再読み込みを選択。
あとはMicrosoftの手順どおりにやれば問題なく完了。
mvvm light toolkitの導入③
今回は別のウィンドウを作成して、
メイン画面から呼び出して表示してみる。
Viewフォルダに、SubView.xamlを
ViewModelフォルダに、SubViewModel.csを追加する。
これらのViewとViewModelの紐付けをするために、
まずはViewModelLocatorにSubViewModelの情報を追加する。
static ViewModelLocator() { ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); if (ViewModelBase.IsInDesignModeStatic) { SimpleIoc.Default.Register<IDataService, Design.DesignDataService>(); } else { SimpleIoc.Default.Register<IDataService, DataService>(); } SimpleIoc.Default.Register<MainViewModel>(); SimpleIoc.Default.Register<SubViewModel>(); }
public SubViewModel Sub { get { return ServiceLocator.Current.GetInstance<SubViewModel>(); } }
次に、SubView.xamlのDataContext属性を修正する。
DataContext="{Binding Sub, Source={StaticResource Locator}}"
SubViewModelに画面表示用にプロパティを用意する。
public class SubViewModel : ViewModelBase { private string _subText; public string SubText { get { return _subText; } set { _subText = value; RaisePropertyChanged("SubText"); } } /// <summary> /// Initializes a new instance of the SubWindowViewModel class. /// </summary> public SubViewModel() { SubText = "これはSubです"; } } }
SubViewに、ViewModelで設定したテキストを表示するようなコントロールを用意してバインドする。
<Grid> <TextBlock Text="{Binding SubText}" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid>
これで紐付けができたので、
App.xamlのStartupUriをSubView.xamlに変更すれば表示できる。
では、別の画面からこのSub画面を表示するにはどうしたらいいか。
まずはApp.xamlのStartupUriをMainWindow.xamlに戻す。
次に、MainViewModelにSub画面表示用のコマンドを用意する。
private RelayCommand _buttonClickCommand; public RelayCommand ButtonClickCommand { get { return _buttonClickCommand ?? (_buttonClickCommand = new RelayCommand(OnButtonClick)); } } public void OnButtonClick() { SubView v = new SubView(); v.ShowDialog(); }
今回はViewModel側でSub画面のViewを生成してそれをShowする方法をとった。
他の方法もあるみたいだけど、今日はこれでいく。
最後に、MianWindow.xamlにボタンを用意して、コマンドを設定する。
<Button Content="Subを表示する" Command="{Binding ButtonClickCommand}" />
mvvm light toolkitの導入②
前回はmvvm lightのテンプレートから生成されたソースの一部を確認しただけでした。
今回は、少しこのソースをいじっていきます。
このテンプレートで個人的に嫌なところは、
MainWindows.xamlがroot直下に配置されていること。
せっかくModelフォルダ・ViewModelフォルダがあるのだから、
Viewフォルダを作ってこっちに移動したい。
というわけで、やります。
これを
こうする。
では、いろいろ微調整。
まず、MainWindow.xamlの名前空間を変える。
Viewフォルダの中への移動なので、MainWindowの前にViewを挿入。
<Window x:Class="MvvmLightIntroduction.View.MainWindow"
次、コードビハインドのMainWindow.xaml.cs
namespace MvvmLightIntroduction.View
続いて、App.xaml。
StartupUriが"MainWindow.xaml"になっているので、"View/MainWindow.xaml"に変更。
StartupUri="View/MainWindow.xaml"
これでひとまずOKなんだけど、
この状態だとMainWindows.xaml内でリソースの参照をしている箇所のディレクトリ階層が不正になるので、
../をつけて正しいパスを参照するように修正する。
<ResourceDictionary Source="../Skins/MainSkin.xaml" />
これでMainWindowのお引越し完了。
mvvm light toolkitの導入①
まずはMVVMのフレームワークの導入から。
Visual Studioの拡張機能で「MVVM light for VS2015」を導入。
その後、新規プロジェクト作成でテンプレートから作成。
作成されたソースを確認。
まず、MainWindow.xaml。
<Window x:Class="MvvmLightIntroduction.MainWindow" 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" xmlns:ignore="http://www.galasoft.ch/ignore" mc:Ignorable="d ignore" Height="300" Width="300" Title="MVVM Light Application" DataContext="{Binding Main, Source={StaticResource Locator}}"> <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Skins/MainSkin.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <Grid x:Name="LayoutRoot"> <TextBlock FontSize="36" FontWeight="Bold" Foreground="Purple" Text="{Binding WelcomeTitle}" VerticalAlignment="Center" HorizontalAlignment="Center" TextWrapping="Wrap" /> </Grid> </Window>
まず、ここでよく分からないのが以下のコード。
mc:Ignorable="d ignore"
これについては、一応説明のページがあった。
mc:Ignorable 属性
katsuyuzu.hatenablog.jp
ようするに、開発中、デザイナ画面では有効になるけど、
実行時には無視してほしい属性に対して使うようなものかな。
このXAMLだと、d:XXXXXXやignore:XXXXXXと書かれたものが実行時に無視されそう。
次、これ。
DataContext="{Binding Main, Source={StaticResource Locator}}"
ViewとViewModelの紐付けにDataContextをバインディングしているのは分かる。
[Main]と書かれているのは、MainViewModelのことかな?[ViewModel]部分は省略して書くのか?
問題は、Source={StaticResource Locator}
これは何だ?ViewModelLocatorと関連あるのか?
そう思ってViewModelLocatorのコードを確認したら、以下のコードがあった。
/// <summary> /// Gets the Main property. /// </summary> [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This non-static member is needed for data binding purposes.")] public MainViewModel Main { get { return ServiceLocator.Current.GetInstance<MainViewModel>(); } }
そしてViewModelLocatorのソースの頭に、コメントでこんなことが書いてあった。
/* In App.xaml: <Application.Resources> <vm:ViewModelLocatorTemplate xmlns:vm="clr-namespace:MvvmLightIntroduction.ViewModel" x:Key="Locator" /> </Application.Resources> In the View: DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}" */
なるほど、LocatorというキーでアプリケーションのリソースにViewModelを登録していて、
Viewからはリソース経由でViewModelと紐付けるのか。
ん?でもこの使い方の説明にあるような、
DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
というのはすんなり理解できるけど、
MainWindow.xamlのほうの
DataContext="{Binding Main, Source={StaticResource Locator}}"
という記述はよく分からない。
Bindingの後って順番関係ないんだっけ?Path=って省略できる?
これについてはこの記事が非常に参考になった。
st63jun.hatenablog.jp
時間が無いので今日はここまで。