none
配列にインデックス指定でバインディングを行うには? RRS feed

  • 質問

  • お世話になります

    現在、非同期で高頻度で転送されてくる 5MB 程度の byte[] 型のデータがあり、
    そのデータをインデックス指定で各バイトデータにバインディングを行いたいです

    バインディングのターゲットは TextBlock とし、およそ 5000個のコントロールをコードで生成しています
    同じくコードで MultiBinding を作成したコントロール分作成し、
    ソースの byte[] 型データ、バインディングするインデックスなどを指定しています

    ここで問題となっているのが、インデックス指定でデータを取得しているため、
    INotifyPropertyChanged を継承するリストを使用しても変更通知が受取れていません
    (テストのため ObservableCollection 型のデータでテストして、コレクション自体の更新通知が受取れていることは確認しています)

    前提条件として、
    ・ソースは byte[] 型のデータ
    ・バイト単位で個別のコントロールにバインド
    ・ソースの変更通知を受け取り、コントロールの値を更新する

    上記を実現する手段をご存知でしたら、お知恵をお借りさせていただければと思います
    以上、よろしくお願いいたします

    環境:
    C#
    .net Framework 4.5
    VisualStudio 2012 Pro

    2013年2月9日 9:38

回答

  • 元コードと再検証で作成したコードの Binding と TextBlock のインスタンスを作成する処理はほぼ同一なのですが、
    元コードでは上記のコード実行時に TestData.PropertyChanged が null のままとなっており、
    このイベントが設定されるための条件がほかにあるのか?
    という質問でした

    ご質問の内容は把握できました。ありがとうございます。
    うまく行っていない最低限のコードが作成できれば、そこから何かわかりそうですね。
    とりあえず検索しただけですが、PropertyChangedがnullにリセットされる場合があるようです。

    PropertyChangedEventHandler is null
    http://stackoverflow.com/questions/14266007/propertychangedeventhandler-is-null


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    • 回答としてマーク Nymphaea 2013年2月17日 13:50
    2013年2月12日 1:39
    モデレータ

すべての返信

  • 質問をさせていただいている内容について、再度検証を行ったところ再検証用コードでは問題なく動作していることが確認できました

    元々が検証用に作成していたコードであり、本件以外の余分なコードが大量にあったため、再度問題個所のみのスモールコードを作成しました
    以下、最低限必要な機能で構成したコード

    <Window x:Class="BindingTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
            <Style x:Key="TestStyle" TargetType="TextBlock">
                <Setter Property="FontFamily" Value="MS Gothic" />
                <Setter Property="FontSize" Value="14" />
            </Style>
        </Window.Resources>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0" Name="StackRoot" Orientation="Vertical" />
            <Button Grid.Row="1" Content="OK" Click="Button_Click" />
        </Grid>
    </Window>
    
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    
    namespace BindingTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            private List<TestData> _data = new List<TestData>();
            private byte _count = 0;
            private const byte SIZE = 20;
    
            public MainWindow()
            {
                InitializeComponent();
    
                this.Loaded += MainWindow_Loaded;
            }
    
            private void MainWindow_Loaded( object sender, RoutedEventArgs e )
            {
                var stack = new StackPanel();
                stack.Orientation = Orientation.Vertical;
    
                for( int i = 0; i < SIZE; i++ ) {
                    _data.Add( new TestData( (byte)(_count + i) ) );
    
                    var bind = new Binding();
                    bind.Source = _data[i];
                    bind.Path = new PropertyPath( "Data" );
                    bind.Converter = new TestConverter();
                    bind.Mode = BindingMode.OneWay;
    
                    var text = new TextBlock();
                    text.SetBinding( TextBlock.TextProperty, bind );
                    text.Style = (Style)Resources["TestStyle"];
                    stack.Children.Add( text );
                }
    
                this.StackRoot.Children.Add( stack );
            }
    
            private void Button_Click( object sender, RoutedEventArgs e )
            {
                _count++;
                for( int i = 0; i < SIZE; i++ ) {
                    _data[i].Data = (byte)(_count + i);
                }
            }
        }
    
        [ValueConversion( typeof( string ), typeof( byte ) )]
        public class TestConverter : IValueConverter
        {
            public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
            {
                var data = (byte)value;
    
                return data.ToString( "X" );
            }
    
            public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
            {
                return null;
            }
        }
    
        public class TestData : INotifyPropertyChanged
        {
            private byte _data = 0;
    
            public TestData( byte data )
            {
                _data = data;
            }
    
            #region INotifyPropertyChanged メンバー
    
            public event PropertyChangedEventHandler PropertyChanged;
            protected void TestDataChanged( string propertyName )
            {
                if( PropertyChanged != null )
                    PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
            }
    
            #endregion
    
            public byte Data
            {
                get { return _data; }
                set
                {
                    if( _data == value )
                        return;
    
                    _data = value;
                    TestDataChanged( "Data" );
                }
            }
        }
    }
    

    上記コードでは、Xaml 側で定義したスタイルを TextBlock に適用し、byte データは ValueConverter で出力形式を変更します
    元のコードでは ObservableCollection 型でテストしていたところを、List<T> 型に変更していますが、この変更による影響はありません

    そこで違いを比較したところ、変更を通知するために INotifyPropertyChanged を継承したクラスで、
    問題となっているコードでは PropertyChanged イベントが設定されていないことがわかりました
    再検証で作成したコードはほぼ元のコードからコピペで作成したものであり、Binding の設定に違いはありません

    Binding 設定時に PropertyChanged イベントが設定されるのには、どのような条件があるのかを調べたのですがまだ有用な情報を見つけられていません
    これらの情報をご存知の方がおられましたら、ご教授いただければと思います

    以上、よろしくお願いいたします

    2013年2月10日 1:30
  • Binding 設定時に PropertyChanged イベントが設定されるのには、どのような条件があるのかを調べたのですがまだ有用な情報を見つけられていません
    これらの情報をご存知の方がおられましたら、ご教授いただければと思います

    ご質問の意味がよくわかりませんでした。「PropertyChangedイベントが設定される」とは何を意味されていますか? PropertyChangedイベントは自分で実装するものですし、そのイベントも自分で発生させるものです。
    また、今回の例はコレクション自体がバインドしているのではありませんので、INotifyPropertyChangedを使わず、依存関係プロパティを使う方法もあります。依存関係プロパティを使う方法が推薦されています。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2013年2月10日 3:03
    モデレータ
  • trapemiya 様 ご返信ありがとうございます

    すみません。読み返してみると確かに意味が通じにくいですね・・・

    「PropertyChangedイベントが設定される」につきましては、
    TestData インスタンスを作成した時点では TestData.PropertyChanged は null ですが、
    TextBlock インスタンスにバインディングをセットする下記コードを実行した際に、
    TestData.PropertyChanged にイベントが設定されます

    text.SetBinding( TextBlock.TextProperty, bind );

    元コードと再検証で作成したコードの Binding と TextBlock のインスタンスを作成する処理はほぼ同一なのですが、
    元コードでは上記のコード実行時に TestData.PropertyChanged が null のままとなっており、
    このイベントが設定されるための条件がほかにあるのか?
    という質問でした

    また、現状各 byte データ個別に INotifyPropertyChanged クラスでコントロールにデータを反映しているため、
    データが 2000 個程度では 100 ミリ秒程度で完了している処理が、10000 個になると 5 秒以上かかってしまっており、
    何らかの対策が必要なので trapemiya 様に提案していただいた、依存関係プロパティについても検討してみたいと思います

    以上、よろしくお願いいたします

    2013年2月10日 3:46
  • こんにちは。

    ひょっとして UIと別のスレッドでソースの変更を行っていませんか?
    別スレッドで発生した PropertyChanged イベントはUIコントロールへ(たしか)伝わらないので、 Dispatcher.Invoke の処理内でソースの変更を行うなどの工夫が必要になります。例えば下記の通り。動作確認はしていませんが大体こんな感じでそこそこ動作すると思います。引数の内容に合わせて foo の引数変更と、 Action を独自のイベントハンドラへ変更してください。

    (旧)
    void foo(){
        [ソース変更を伴う処理]
    }

    (新)
    void foo(){
        var dispatcher = Application.Current.Dispatcher;

        if ( dispatcher.CheckAccess() ){
            [ソース変更を伴う処理]
        }
        else{
            dispatcher.Invoke( new Action( () => {
                foo();
            }), null );
        }
    }

    2013年2月10日 4:27
  • 元コードがどうかわからないですが、INotifyCollectionChangedやBinding.IndexerNameの通知をしてなかったり。
    5MBもバイト単位でバインドはいくらなんでも無茶なので、バインドする範囲を制限するようにした方がいいと思います。

    namespace WpfApplication1
    {
        using System;
        using System.Collections.Generic;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Data;
        using System.ComponentModel;
        using System.Collections.Specialized;
    
        public partial class MainWindow : Window
        {
            private const int SIZE = 10000;
    
            public MainWindow()
            {
                InitializeComponent();
                this.Loaded += MainWindow_Loaded;
            }
    
            private void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                array = new byte[SIZE];
                dummyArray = new NotifyDummyList<byte>(array, 0, SIZE - 1);
    
                var stack = new StackPanel();
                stack.Orientation = Orientation.Vertical;
    
                for (int i = 0; i < SIZE; i++)
                {
                    var bind = new Binding();
                    bind.Source = dummyArray;
                    bind.Path = new PropertyPath("["+i+"]");
                    bind.Mode = BindingMode.OneWay;
                    var text = new TextBlock();
                    text.SetBinding(TextBlock.TextProperty, bind);
                     stack.Children.Add(text);
                }
    
                this.StackRoot.Children.Add(stack);
            }
    
            private byte[] array;
            private NotifyDummyList<byte> dummyArray;
    
            private void Button_Click1(object sender, RoutedEventArgs e)
            {
                //データを変更
                dummyArray[1] = 5;
            }
            private void Button_Click2(object sender, RoutedEventArgs e)
            {
                //データを一括変更
                dummyArray.Replace(new byte[] { 1, 2 }, 0);
            }
            private void Button_Click3(object sender, RoutedEventArgs e)
            {
                //元の配列を変更してから通知
                array[1] = 10;
                array[3] = 100;
                dummyArray.RaiseReplace(1, 3);
            }
            private void Button_Click4(object sender, RoutedEventArgs e)
            {
                //開始位置を変更
                dummyArray.ChangeIndex(1, SIZE - 2);
            }
        }
    
        class NotifyDummyList<T> : IList<T>,INotifyCollectionChanged, INotifyPropertyChanged
        {
            public NotifyDummyList(int count) : this(new T[count],0,count-1)
            {
            }
            public NotifyDummyList(T[] data, int start, int end)
            {
                this.data = data;
                ChangeIndex(start, end);
            }
    
            public T[] InnerArray
            {
                get
                {
                    return data;
                }
            }
            protected T[] data;
            protected int start;
            protected int end;
            protected int count;
            protected int ToDataIndex(int index)
            {
                int indexX = index + start;
                if (start <= indexX && indexX <= end)
                {
                    return indexX;
                }
                else
                {
                    throw new ArgumentOutOfRangeException();
                }
            }
    
            public   T this[int index]
            {
                get
                {
                    return data[ToDataIndex(index)];
                }
                set
                {
                    Replace(new T[] { value }, index);
                }
            }
            public void Replace(T[] newValeus, int index)
            {
                int count = newValeus.Length;
                int indexData = ToDataIndex(index);
                int indexeEnd = ToDataIndex(index + count);
    
                T[] oldValues = new T[count];
                Array.Copy(data, indexData, oldValues, 0, count);
                Array.Copy(newValeus, 0, data, indexData, count);
                OnPropertyChanged(Binding.IndexerName);
                NotifyCollectionChangedEventArgs e
                    = new NotifyCollectionChangedEventArgs
                        (NotifyCollectionChangedAction.Replace, newValeus, oldValues, index);
            }
    
            public void RaiseReplace(int start, int end)
            {
                NotifyDummyList<T> dummy = new NotifyDummyList<T>(data, start, end);
    
                OnPropertyChanged(Binding.IndexerName);
                NotifyCollectionChangedEventArgs e
                    = new NotifyCollectionChangedEventArgs
                        (NotifyCollectionChangedAction.Replace, dummy, dummy, start);
            }
    
            public void ChangeIndex(int start, int end)
            {
                this.start = start;
                this.end = end;
                this.count = end - start + 1;
                if (count < 0)
                {
                    count = 0;
                }
                NotifyCollectionChangedEventArgs e
                    =new NotifyCollectionChangedEventArgs
                        (NotifyCollectionChangedAction.Reset);
                OnCollectionChanged(e);
                OnPropertyChanged(Binding.IndexerName);
            }
    
            #region INotifyCollectionChanged メンバー
    
            public event NotifyCollectionChangedEventHandler CollectionChanged;
            protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
            {
                var cc = CollectionChanged;
                if (cc != null)
                {
                    cc(this, e);
                }
            }
            #endregion
    
            #region INotifyPropertyChanged メンバー
    
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged(string name)
            {
                var pc = PropertyChanged;
                if (pc != null)
                {
                    pc(this, new PropertyChangedEventArgs(name));
                }
            }
            #endregion
    
            #region IList<T> メンバー
    
            int IList<T>.IndexOf(T item)
            {
                throw new NotImplementedException();
            }
    
            void IList<T>.Insert(int index, T item)
            {
                throw new NotImplementedException();
            }
    
            void IList<T>.RemoveAt(int index)
            {
                throw new NotImplementedException();
            }
            #endregion
    
            #region ICollection<T> メンバー
    
            void ICollection<T>.Add(T item)
            {
                throw new NotImplementedException();
            }
    
            void ICollection<T>.Clear()
            {
                throw new NotImplementedException();
            }
    
            bool ICollection<T>.Contains(T item)
            {
                throw new NotImplementedException();
            }
    
            void ICollection<T>.CopyTo(T[] array, int arrayIndex)
            {
                throw new NotImplementedException();
            }
    
            int ICollection<T>.Count
            {
                get { return count; }
            }
    
            bool ICollection<T>.IsReadOnly
            {
                get { return true; }
            }
    
            bool ICollection<T>.Remove(T item)
            {
                throw new NotImplementedException();
            }
    
            #endregion
    
            #region IEnumerable<T> メンバー
    
            public IEnumerator<T> GetEnumerator()
            {
                for (int i = start; i <= end; i++)
                {
                    yield return data[i];
                }
            }
    
            #endregion
    
            #region IEnumerable メンバー
    
            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                for (int i = start; i <= end; i++)
                {
                    yield return data[i];
                }
            }
    
            #endregion
        }
    }

    別スレッドからでも変更できますよ。
    #ItemsControlなどにコレクションをバインドするとインデックス同期のために別スレッドから変更できないことはありますが。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2013年2月10日 4:55
  • HIDE0707 様 ご返信ありがとうございます

    ソースの更新につきましては、元コードでも更新スレッドの対策は行っており、
    デバッガを使って確認もしてみたところ UI スレッドで更新していることを確認しています

    ご確認ありがとうございました

    2013年2月11日 1:45
  • gekka 様 ご返信ありがとうございます

    サンプルコードまで作成していただき、ありがとうございます
    参考にさせていただきます

    コントロールにつきましては、5MB すべてに byte 単位でバインドするわけではなく、
    UI の操作によりバインドするコントロールの個数が 1000~2000 個程度増減しますが
    最初の投稿にある通り概ね 5000 個程度になります

    以上、よろしくお願いします

    2013年2月11日 2:20
  • 元コードと再検証で作成したコードの Binding と TextBlock のインスタンスを作成する処理はほぼ同一なのですが、
    元コードでは上記のコード実行時に TestData.PropertyChanged が null のままとなっており、
    このイベントが設定されるための条件がほかにあるのか?
    という質問でした

    ご質問の内容は把握できました。ありがとうございます。
    うまく行っていない最低限のコードが作成できれば、そこから何かわかりそうですね。
    とりあえず検索しただけですが、PropertyChangedがnullにリセットされる場合があるようです。

    PropertyChangedEventHandler is null
    http://stackoverflow.com/questions/14266007/propertychangedeventhandler-is-null


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    • 回答としてマーク Nymphaea 2013年2月17日 13:50
    2013年2月12日 1:39
    モデレータ
  • trapemiya 様 ご返信ありがとうございます

    返信が遅くなってしまい、申し訳ありません
    こちらでも検索をしてみると結構な数がヒットしたので調査をしてみたところ、
    INotifyPropertyChanged を継承したクラスを List<T> などに一旦格納したものを Binding.Source で参照し、
    PropertyPath で変更を通知する型を指定してあげることで PropertyChanged がセットされるようです
    INotifyPropertyChanged を継承したクラスを直接 Binding.Source で参照した場合に null になるようなのですが、
    なぜリセットされるのかはまだわかりませんでした

    別件で時間が取れず、元ソースとの比較はまだ行っていないのですが、
    検証の内容が処理速度の改善になってきていることと、平日は時間が取れず返信が遅くなってしまうため、
    本件は一旦クローズとさせていただきます

    ご協力くださった皆様、ありがとうございました

    2013年2月17日 13:49