読者です 読者をやめる 読者になる 読者になる

残り僅かな全盛期

30代修行中の初心者プログラマー。いろいろなサイトを参考にコードを使用させてもらいます。

添付プロパティと添付ビヘイビア

添付プロパティについて調べてみた。

参考にしたサイトはこれ。
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();
}

参考にさせていただいたブログは以下。
ありがとうございました。

yohshiy.blog.fc2.com

qiita.com

非同期処理のいろいろ

初心者プログラマーにとって、
非同期処理はとてもハードルが高い。
方法が一つだけならまだしも、
いろいろな手法があって自分の中でなかなか整理できない。

ということで実際にコードを書いてまとめてみた。

参考にした本は、これ。
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フォルダを作ってこっちに移動したい。

というわけで、やります。

これを
f:id:melon-panda:20170417180453j:plain

こうする。
f:id:melon-panda:20170417180518j:plain


では、いろいろ微調整。
まず、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

時間が無いので今日はここまで。