やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

WPFパフォーマンス改善その1. 初期表示(描画)速度の改善策

初期表示(描画)速度の改善策
ここでは初期表示、つまり画面をインスタンス化してから描画されるまでの時間を短縮するための改善策や
画面の再描画処理時間の短縮方法を記述します。

続きを読む

WPFパフォーマンスの調整について

先日、仕事でWPF のパフォーマンス調整をしました。
その際に参考になった記事や対策をまとめました。


ここでは、WPFのパフォーマンス改善のためのガイドラインやTipsなどを記述します。
WPFの動作が遅くなる理由は多岐にわたり、かつ複合的な要因がある場合が殆どです。
改善策は1つだけでなく、複数の対策を講じてください。

続きを読む

StoryboardのStopに失敗する場合の対処法

WPFでStoryboardをコードビハインドで終了させる際に、詰まったポイントをメモメモ

private void StartButton_Click(object sender, EventArgs e)
{
    // storyboard には既にリソース上のStoryboard を設定しているものとする。
    storyboard.Start(Ellipse1);
}

private void EndButton_Click(object sender, EventArgs e)
{
    // ここでアニメーションを停止させたい。
    storyboard.Stop(Ellipse1);
}

このようなコードがあったとして、実行すると以下のメッセージが表示されました。

System.Windows.Media.Animation Warning: 6 : Unable to perform action because the specified Storyboard was never applied to this object for interactive control.; Action='Stop';

なんじゃこりゃ
と思いいろいろ調べてみましたが、以下のページが参考になりました。

stackoverflow.com

警告メッセージをざっくり訳すと、
"Storyboardが対話制御設定されていないからStop動作は実行できないよ。"
とのこと。

で、上記のページを見るとBeginの第三引数に対話制御を可能とするかどうかのフラグを設定せよ とのこと。
なので正しいコードはこう↓↓↓

private void StartButton_Click(object sender, EventArgs e)
{
    // storyboard には既にリソース上のStoryboard を設定しているものとする。
    storyboard.Start(Ellipse1, true);
}

private void EndButton_Click(object sender, EventArgs e)
{
    // ここでアニメーションを停止させたい。
    storyboard.Stop(Ellipse1);
}

これが例外も発行しないし、出力ウィンドウにささやかなメッセージだけを残すので全く気が付かなかった。
というか、メッセージに気がついても修正方法がわかんなかった。

DataGridColumnにデータバインドするには

業務アプリを制作する際にDataGridのある列の表示非表示を切り替えたいケースがあると思います。
表示だけでなく、ヘッダーや幅を変えたいなどの場合も同様です。
しかし、少し手を加えなければDataGridColumn のバインディングは不可能です。
(なぜかは後述します。)

このような場合、上記の仕組みを実現するには主に以下の2通りがあります。

1.画面(View)側でDataGridColumn コントロールにアクセス
2.ViewModel から表示プロパティをバインド

1の場合は対応する画面が一つだけなら問題無いでしょう。
しかし、画面が複数あったりした場合はどうでしょう?
いやいやWPFらしくデータバインドを使いたい、と思う方もいらっしゃるでしょう。
ということで、今回はケース2.の方法を実現しようと思います。


■参考資料
http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/

DataGridColumn には間をとりもつ存在が必要

参考資料より引用

I don’t know the exact mechanism that enables this behavior, but we’re going to take advantage of it to make our binding work…


オチから説明すると、なぜDataGridColumn とデータバインドできない原因は明確にはわかっていません。
上記Blogを読むに、論理ツリー内にDataGridColumn が含まれていないからでは?ということみたいです。
DataGridColumn を論理ツリー内に含むにはFreezable オブジェクトとバインドするのだそうです。

以下は、ViewModel の間をとりもつBindingProxy クラスの実装です。

< BindingProxy.cs >

 using System.Windows;

namespace DataGridColumnBinding
{
    /// <summary>
    /// ViewModelのバインディングソースの代理として働くクラスです。
    /// </summary>
    public class BindingProxy : Freezable
    {
        /// <summary>
        /// Freezableオブジェクトのインスタンスを生成します。
        /// </summary>
        /// <returns></returns>
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }
        
        /// <summary>
        /// 間をとりもつプロパティ
        /// データバインドした場合は、このプロパティがViewModelの代わりになる。
        /// </summary>
        public object Data
        {
            get { return (object) GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }
        
        /// <summary>
        /// Data の依存関係プロパティ定義
        /// </summary>
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof (object), typeof (BindingProxy), new UIPropertyMetadata(null));
    }
}


これに対し、Xaml とViewModel の定義は次のようになります。< MainWindow.xaml >

<Window x:Class="DataGridColumnBinding.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:local="clr-namespace:DataGridColumnBinding"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="525"
        Height="350"
        Loaded="MainWindow_OnLoaded"
        d:DataContext="{d:DesignInstance {x:Type local:MainWindowViewModel}, IsDesignTimeCreatable=True}"
        mc:Ignorable="d">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel Margin="12" Orientation="Horizontal">
            <CheckBox Content="VisibleDate" IsChecked="{Binding IsVisibleDate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <CheckBox Content="VisibleTime" IsChecked="{Binding IsVisibleTime, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        <Border Grid.Row="1">
            <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Times, Mode=OneWay}">
                <DataGrid.Resources>
                    <!--
                        BindingProxy のリソースを定義
                        Data には、MainWindowViewModel がバインドされる。
                    -->
                    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
                </DataGrid.Resources>
                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding Today, Mode=OneWay, StringFormat=\{0:d\}}"
                                        Header="Date"
                                        Visibility="{Binding Data.IsVisibleDate, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay, Source={StaticResource Proxy}}" />
                    <DataGridTextColumn Binding="{Binding Mode=OneWay, StringFormat=\{0:hh:mm:ss\}}"
                                        Header="Time"
                                        Visibility="{Binding Data.IsVisibleTime, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay, Source={StaticResource Proxy}}" />
                    <DataGridTextColumn Binding="{Binding Mode=OneWay, StringFormat=\{0:G\}}" Header="DateTime" />
                </DataGrid.Columns>
            </DataGrid>
        </Border>
    </Grid>
</Window>

< MainWindowViewModel.cs >

 using System;
using System.Collections.ObjectModel;
using System.Linq;
using Microsoft.Practices.Prism.Mvvm;

namespace DataGridColumnBinding
{
    public class MainWindowViewModel : BindableBase
    {
        public MainWindowViewModel()
        {
            IsVisibleDate = true;
            IsVisibleTime = true;

            Times = new ObservableCollection<DateTime>();

            foreach (var value in Enumerable.Range(0, 20))
            {
                Times.Add(DateTime.Now.AddHours(value));
            }
        }

        /// <summary>
        /// 日付を表示するかどうか
        /// </summary>
        private bool _isVisibleDate;

        /// <summary>
        /// 日付を表示するかどうか
        /// </summary>
        public bool IsVisibleDate
        {
            get { return _isVisibleDate; }
            set { SetProperty(ref _isVisibleDate, value); }
        }

        /// <summary>
        /// 時刻を表示するかどうか
        /// </summary>
        private bool _isVisibleTime;

        /// <summary>
        /// 時刻を表示するかどうか
        /// </summary>
        public bool IsVisibleTime
        {
            get { return _isVisibleTime; }
            set { SetProperty(ref _isVisibleTime, value); }
        }

        public ObservableCollection<DateTime> Times { get; private set; }
    }
}


ポイントは以下の2箇所
1.BindingProxy リソースの定義

<local:BindingProxy x:Key="Proxy" Data="{Binding}" />


2.データバインドの定義

<DataGridTextColumn Binding="{Binding Today, Mode=OneWay, StringFormat=\{0:d\}}"
                    Header="Date"
                    Visibility="{Binding Data.IsVisibleDate, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay, Source={StaticResource Proxy}}" />

Visibility の設定を上記のように定義することでデータバインドが可能になります。
画面を表示すると以下のようになります。

f:id:iyemon018:20160130234226p:plain

f:id:iyemon018:20160130234229p:plain

f:id:iyemon018:20160130234230p:plain



このようにDataGridColumn のプロパティもデータバインドが可能になっています。
今回のサンプルソースは以下から↓↓↓
github.com

シリアライザ~BinaryFormatter~

前回リアライザについての説明を軽くしました。
今回はその中の一つ、BinaryFormatterについて説明していこうと思います。

特徴

MSDNのページはこちら↓↓↓
BinaryFormatter クラス (System.Runtime.Serialization.Formatters.Binary)
BinaryFormatterはその名の通り、オブジェクトをバイナリに書式化します。
さらに、たいていの型に対応しているため、とりあえずシリアライズするならこれを使用する場合が多いでしょう。
パフォーマンス的にも高速です。が、あるケースによっては速度が激遅になるんだとか…。
詳しくは以下のページをご覧いただけるとわかるはずです。
neue cc - .NET(C#)におけるシリアライザのパフォーマンス比較

使い方

    public static T DeepCopy<T>(this T self)
    {
        T result;

        using (var ms = new MemoryStream())
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(ms, self);
            ms.Position = 0;
            result = (T)formatter.Deserialize(ms);
        }

        return result;
    }

これは拡張メソッドで何でもコピー可能にしていますが、シリアライズしたいオブジェクトとStreamでシリアライズし、その結果をデシリアライズします。
そうすることでオブジェクトのコピーが可能になっています。

さらに、シリアライズするクラスにはSerializableAttributeを定義する必要があります。もちろん基底クラスにもです。

シリアライズ元となるクラスは以下のようにしました。
TimeTracerは処理速度を出力するための機能です。
<ViewModelBase.cs>

    /// <summary>
    /// すべてのViewModelの基底クラスです。
    /// </summary>
    [Serializable]
    public abstract class ViewModelBase : BindableBase
    {
        /// <summary>
        /// オブジェクトの状態を保存するためのスナップショットデータです。
        /// </summary>
        private ViewModelBase _snapshot;

        /// <summary>
        /// スナップショットデータを保存します。
        /// </summary>
        public void SaveSnapshotData()
        {
            using (new TimeTracer("オブジェクトのコピー"))
            {
                _snapshot = this.DeepCopy();
            }
        }

        /// <summary>
        /// オブジェクトが更新されたかどうかを判定します。
        /// </summary>
        /// <returns>
        /// オブジェクトが更新されている場合は、true を
        /// 更新されていない場合は、false を返します。
        /// </returns>
        public bool IsUpdate()
        {
            using (new TimeTracer("更新チェック"))
            {
                if (_snapshot == null)
                {
                    return true;
                }

                return !this.DeepCopy().Equals(_snapshot);
            }
        }

        /// <summary>
        /// オブジェクトの状態を元に戻します。
        /// </summary>
        public void Reset()
        {
            using (new TimeTracer("オブジェクトにリセット"))
            {
                if (_snapshot != null)
                {
                    UpdateObject(_snapshot);
                }
            }
        }

        /// <summary>
        /// オブジェクトを更新します。
        /// </summary>
        /// <param name="obj">オブジェクトデータ</param>
        protected virtual void UpdateObject(ViewModelBase obj)
        {

        }
    }

<BinaryFormatterViewModel.cs>

    /// <summary>
    /// BinaryForamtterView用のViewModelクラスです。
    /// </summary>
    [Serializable]
    public class BinaryFormatterViewModel : ViewModelBase
    {
        private int _age;

        private string _name;

        private DateTime _birthday;

        private BloodType _bloodType;

        private Gender _gender;

        public BinaryFormatterViewModel()
        {
            Age = 20;
            Name = string.Empty;
            Birthday = new DateTime(1990, 1, 1);
            BloodType = SelializeSample.BloodType.TypeA;
            Gender = SelializeSample.Gender.Male;
        }

        /// <summary>年齢</summary>
        public int Age
        {
            get { return _age; }
            set { SetProperty<int>(ref _age, value); }
        }

        /// <summary>名前</summary>
        public string Name
        {
            get { return _name; }
            set { SetProperty<string>(ref _name, value); }
        }

        /// <summary>誕生日</summary>
        public DateTime Birthday
        {
            get { return _birthday; }
            set { SetProperty<DateTime>(ref _birthday, value); }
        }

        /// <summary>血液型</summary>
        public BloodType BloodType
        {
            get { return _bloodType; }
            set { SetProperty<BloodType>(ref _bloodType, value); }
        }

        /// <summary>性別</summary>
        public Gender Gender
        {
            get { return _gender; }
            set { SetProperty<Gender>(ref _gender, value); }
        }

        /// <summary>
        /// オブジェクトを更新します。
        /// </summary>
        /// <param name="obj">オブジェクトデータ</param>
        protected override void UpdateObject(ViewModelBase obj)
        {
            base.UpdateObject(obj);

            var vm = obj as BinaryFormatterViewModel;
            if (vm != null)
            {
                Age       = vm.Age;
                Name      = vm.Name;
                Birthday  = vm.Birthday;
                BloodType = vm.BloodType;
                Gender    = vm.Gender;
            }
        }

        /// <summary>
        /// オブジェクト同士を比較します。
        /// </summary>
        /// <param name="obj">比較対象のオブジェクト</param>
        /// <returns>true:一致, false:不一致</returns>
        public override bool Equals(object obj)
        {
            var other = obj as BinaryFormatterViewModel;
            if (other == null)
            {
                return false;
            }
            return Age == other.Age
                 & Name.Equals(other.Name)
                 & Birthday == other.Birthday
                 & BloodType == other.BloodType
                 & Gender == other.Gender;
        }
    }

画面で使用する場合はこのようにしました。
<BinaryFormatterView.xaml.cs>

    /// <summary>
    /// BinaryFormatterView.xaml の相互作用ロジック
    /// </summary>
    public partial class BinaryFormatterView : Window
    {
        /// <summary>
        /// 当画面のViewModel
        /// </summary>
        private BinaryFormatterViewModel _vm;

        public BinaryFormatterView()
        {
            InitializeComponent();
        }

        /// <summary>
        /// リセットボタンをクリックした際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void ResetButton_Click(object sender, RoutedEventArgs e)
        {
            // リセット
            _vm.Reset();
        }

        /// <summary>
        /// 戻るボタンをクリックした際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void BackButton_Click(object sender, RoutedEventArgs e)
        {
            if (_vm.IsUpdate())
            {
                var result = MessageBox.Show("データが更新されています。\r\n入力内容を破棄しますか?"
                                            , "確認"
                                            , MessageBoxButton.YesNo
                                            , MessageBoxImage.Question
                                            , MessageBoxResult.No);
                if (result != MessageBoxResult.Yes)
                {
                    return;
                }
            }
            Close();
        }

        /// <summary>
        /// 当ウィンドウがロードされた際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _vm = DataContext as BinaryFormatterViewModel;
            if (_vm != null)
            {
                // これ以降に変更されたら変更ダイアログを表示します。
                _vm.SaveSnapshotData();
            }
        }
    }

画面イメージはこんな感じです。
f:id:iyemon018:20151223181836p:plain

これに値を入力し、
f:id:iyemon018:20151223181912p:plain

戻るボタンをクリックすると、ダイアログが表示されます。
f:id:iyemon018:20151223181940p:plain


さて、シリアライズの話と言いつつ話の本筋はオブジェクトのコピーになっていますがそれはさておき。
このように属性を付加するだけでオブジェクトのコピーが可能になるのがBinaryFormatterの強みです。

今回使用したサンプルはこちら↓↓↓
github.com