ページ 11

C#のデータバインディングコントロールについて教えてください

Posted: 2012年9月23日(日) 10:28
by taketoshi
フォームにラベルを作り、そこに日付を表示させようと思っています。
タイマーイベントを1秒間隔で設定し、プロパティ内のセットアクセサで日付と時間を取得しています。

データバインディングコントロールをラベルにバインドし、プロパティが書き換えられると同時に
バインドさせたラベルの文字列を書き換えて時計を作ろうかと思ったのですが
起動した時刻が表示されたっきりで、そのあとラベルのテキストが更新されません

また、コンストラクタに

コード:

propstr = "";
のような初期化の記述をしないと、起動時の時刻も表示されません。

データバインディングは双方向で表示と変数の設定をしてくれると思ったのですが違うのでしょうか。
ぜひご教授をお願いします。

コード:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace 時計の政策
{

    public partial class Form1 : Form
    {

        private Clock _cl;

        public Form1()
        {

            InitializeComponent();

        }

        //タイマーイベント
        private void timer1_Tick(object sender, EventArgs e)
        {
            _cl.GetDate();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //タイマーの設定
            timer1.Interval = 1000;//1秒に一回
            timer1.Start();
            _cl = new Clock();
            //データソースの設定
            clockBindingSource.DataSource = _cl;
        }

    }

    public class Clock{

        private string str;

        //ラベルに表示するプロパティ
        public string propstr{
            set { str = DateTime.Now.ToString();}
            get { return str; }
        }

        //コンストラクタ
        public Clock(){
            propstr = "";
        }

        //イベントを宣言
        #region INotifyPropertyChanged メンバ

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //イベントの実装
        public void NotifyPropertyChanged(string info)
        {
            if (PropertyChanged != null)
            {
                //デリゲートを介してイベントを発生させる
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        //日付の取得
        public void GetDate(){
            //イベントの発生
            propstr = "";
            NotifyPropertyChanged("propstr");
        }
    }
}


Re: C#のデータバインディングコントロールについて教えてください

Posted: 2012年9月23日(日) 13:38
by YuO
taketoshi さんが書きました:データバインディングコントロールをラベルにバインドし、プロパティが書き換えられると同時に
バインドさせたラベルの文字列を書き換えて時計を作ろうかと思ったのですが
起動した時刻が表示されたっきりで、そのあとラベルのテキストが更新されません
Windows Formsのデータバインディングでバインドソースからコントロールの表示が更新されるのは,
  • INotifyPropertyChanged.PropertyChangedイベントがソースで発生し,イベントオブジェクトPropertyNameプロパティがバインドしているプロパティの名前と一致する場合
  • バインドしているプロパティの名前の末尾にChangedを付けた名前のイベント (ValueプロパティならValueChangedイベント) がソースに存在し,そのイベントが発生した場合
  • コントロールの対象プロパティ用のBindingインスタンスのReadValueメソッドを呼び出した場合
  • BindingSourceクラスを介している場合,そのインスタンスのResetBindingsメソッドを呼び出した場合
あたりになります。
今回の場合,

コード:

        public event PropertyChangedEventHandler PropertyChanged;
とPropertyChangedイベントを定義しているにも関わらず,

コード:

    public class Clock{
とINotifyPropertyChangedを継承していません。
これでは,上記の最初の条件が成立しません。よって,コントロールがソースの変更情報を取得することが出来ません。

taketoshi さんが書きました:また、コンストラクタに

コード:

propstr = "";
のような初期化の記述をしないと、起動時の時刻も表示されません。
propstrの元になる値であるstrは,propstrプロパティのsetでのみ設定されます。
このため,propstrに値を代入しないと当然ながらstrの値,つまりはpropstrの値はnullのままです。

あとは……
  • プロパティに代入した値が常に無視されるような場合は,そもそもプロパティのsetメソッドを用意しない方がよいです。
    通常は代入した値がそのまま保持し,範囲外の値の場合に例外を出すか範囲内に丸める程度に留めるのがよいでしょう。
  • PropertyChangedイベントを呼び出すメソッドは,プロパティのsetメソッドの中で呼ぶ方がよいです。
    そちらの方が,.NET Framework 4.5のCallerMemberName属性の恩恵にあずかれます。
あたりでしょうか。
# 私の場合だと,Clock自体がTimer持っていて,SynchronizationContextを経由してPropertyChangedイベントを発生させる形式にしますが。

Re: C#のデータバインディングコントロールについて教えてください

Posted: 2012年9月23日(日) 14:45
by taketoshi
Yuoさんご解説ありがとうございます。

iNotifyPropertyChangedを継承したところ、うまく動作しました。
propstrの内容取得箇所を変更し、プロパティ内でイベントを発生させるように書き直してみました。
アドバイスいただいた通り、clockにタイマーを持たせてクロック内ですべて処理できるようになりました。

かなりすっきりしたコードになったと思います。ありがとうございます

コード:

using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace 時計の政策
{

    public partial class Form1 : Form
    {

        private Clock _cl;

        public Form1()
        {

            InitializeComponent();

        }

        private void Form1_Load(object sender, EventArgs e)
        {
            _cl = new Clock();
            //データソースの設定
            clockBindingSource.DataSource = _cl;
        }

    }

    public class Clock:INotifyPropertyChanged{

        private string str;
        Timer _tm;

        //ラベルに表示するプロパティ
        public string propstr{
            set
            {   str = value;
                NotifyPropertyChanged("propstr");
            }
            get { return str; }
        }

        //コンストラクタ
        public Clock(){
            //タイマーの起動
            _tm = new Timer();
            _tm.Interval = 1000;
            _tm.Start();
            //タイマーイベントを登録
            _tm.Tick += new EventHandler(timer_tick);
            //初期値の設定
            propstr = DateTime.Now.ToString();
        }

        //イベントを宣言
        #region INotifyPropertyChanged メンバ

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        //タイマーイベント
        public void timer_tick(object sender,EventArgs e){
            this.GetDate();
        }

        //イベントの実装
        public void NotifyPropertyChanged(string info)
        {
            if (PropertyChanged != null)
            {
                //デリゲートを介してイベントを発生させる
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        //日付の取得
        public void GetDate(){
            propstr = DateTime.Now.ToString();         
        }
    }
}


Re: C#のデータバインディングコントロールについて教えてください

Posted: 2012年9月24日(月) 01:37
by YuO
誤解させてしまったかもしれませんが,
YuO さんが書きました:# 私の場合だと,Clock自体がTimer持っていて,SynchronizationContextを経由してPropertyChangedイベントを発生させる形式にしますが。
の「Timer」はWinFormsのTimerではなく,System.Timers.Timerの方です。
このタイマーはスレッドを使うので,SynchronizationContextを経由してUIスレッドでイベントを発生させる,という方法になっています。

SynchronizationContextを使うのは,
  • System.Windows.Forms.Timerを使うと,WinFormsという特定のUIライブラリに依存してしまう
  • 上記を避けるために,System.Timers.Timerを使う
    • System.Timers.Timerはスレッドプールを使う
    • PropertyChangedイベントを非UIスレッドで実行すると,WinFormsではクロススレッドの問題が発生する
      • UIスレッドで実行するためにControl.Invokeを使う
      • ……WinFormsに結局依存してしまう
という状況をさけるためです。
SynchronizationContextはUIがスレッドにひも付いた後であれば,WinForms / WPF / WinRTなどUIに依存せず使えます。
# まぁ,WinRTではSystem.Timers.TimerもSystem.Threading.Timerも使えませんが……。

コード:

// #define USE_CALLER_INFORMATION // .NET 4.5以降でCALLER INFORMATIONを使う場合はコメントを外す

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Timers;

// System.Timers.TimerとSystem.Threading.TimerがあるのでTimerに別名をつけておく
using Timer = System.Timers.Timer;

// TimerがMarshalByRef,つまりIDisposableであるためIDisposableを継承してDispose可能にしておく
public class Clock : INotifyPropertyChanged, IDisposable
{
    private string _str;
    private SynchronizationContext _context;
    private Timer _timer;

    public Clock ()
    {
        _context = SynchronizationContext.Current;
        GetDate();
        _timer = new Timer(1000);
        _timer.Elapsed += TimerElapsed;
        _timer.Enabled = true;
    }

    public string PropStr
    {
        get
        {
            if (_context == null)
            {
                // コンストラクタでキャプチャできなかった場合はgetで再キャプチャしてみる
                _context = SynchronizationContext.Current; 
            }

            return _str;
        }
        private set
        {
            _str = value;
#if USE_CALLER_INFORMATION
            RaisePropertyChanged();
#else
            // リファクタリング時に引数を書き換える必要あり。
            // Expressionとかを使って無理矢理対応させる方法はあるけれども……。
            RaisePropertyChanged("PropStr");
#endif
        }
    }

#if USE_CALLER_INFORMATION
    private void RaisePropertyChanged ([CallerMemberName] string propertyName = null)
#else
    private void RaisePropertyChanged (string propertyName)
#endif
    {
        // 同期コンテキストを取得できない場合は直接呼び出し,取得できた場合は同期コンテキスト経由で呼び出す
        if (_context == null)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }
        else
        {
            // WinRTでは同期であるSendが使えないため非同期のPostを使う必要あり。
            _context.Send(state => OnPropertyChanged(new PropertyChangedEventArgs(propertyName)), null);
        }
    }

    protected virtual void OnPropertyChanged (PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, e);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void TimerElapsed (object sender, ElapsedEventArgs e)
    {
        GetDate();
    }

    private void GetDate ()
    {
        PropStr = DateTime.Now.ToString();
    }

// 以下,Disposeパターン
    ~Clock ()
    {
        Dispose(false);
    }

    protected virtual void Dispose (bool disposing)
    {
        if (disposing)
        {
            _timer.Dispose();
        }
    }

    public void Dispose ()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}