none
WPF XAMLから複数のCLRインスタンスへのバインド RRS feed

  • 質問

  • [質問]
    TextBox1のTextには、_sample1インスタンスのMessageプロパティ、FontSizeには、_sample2インスタンスのMessageプロパティをバインドする方法がありますか?

    捕捉説明

    データコンテキストに一方のインスタンスを設定すると、もう一方が参照できず困っています。
     下の例題は、データコンテキストの_sample1を設定しているため、_sample1のインスタンスが設定され、起動時にフォントサイズ20Fの"Sample1 Start!"が表示され、ボタンをクリックすると、フォントサイズ40Fの"Sample1 Start!"に変化します。メッセージはそのままで、フォントサイズを_sample2を参照するように変更したい。つまり、起動時は、フォントサイズ50Fでボタンをクリックするとフォントサイズ100Fに変化することになります。

    データコンテキストに1つのインスタンスを設定して、XAMLからプロパティ名のみ指定する方法では対応できないように思えます。このように複数のCLRインスタンスを同時に参照したい場合、どのようにして解決するかが質問の意図です。

    SampleClassと_sample1および_sample2インスタンスは、説明のためむりやり作成したデータ構造です。このデータ構造を見直すことで、結果的に上記の問題が回避できるかも知れませんが、あくまでこの基本構造を維持しつつ解決したいというのが意図です。

    CLRインスタンスを直接バインドすることがどうしてもできない場合は、何かを仲介して間接的に参照するアイデアでも構いませんが、それでも、UIとソースの分離というWPFの目標は維持したいし、複雑化によるコードの可読性の低下も最小限度に抑えたいと思っています。


    以下、例題

    [C#]

    using System;
     using System.Collections.Generic;
     using System.ComponentModel;
     using System.Linq;
     using System.Text;
     using System.Threading.Tasks;
     using System.Windows;
     using System.Windows.Controls;
     using System.Windows.Data;
     using System.Windows.Documents;
     using System.Windows.Input;
     using System.Windows.Markup;
     using System.Windows.Media;
     using System.Windows.Media.Imaging;
     using System.Windows.Navigation;
     using System.Windows.Shapes;

    namespace BindingTest
     {
         /// <summary>
         /// MainWindow.xaml の相互作用ロジック
        /// </summary>
         public partial class MainWindow : Window
         {
             public MainWindow()
             {
                 InitializeComponent();
                 _sample1 = new SampleClass("Sample1",20.0F);
                 _sample2 = new SampleClass("Sample2",50.0F);
                 this.DataContext = _sample1;
             }
             private SampleClass _sample1;
             private SampleClass _sample2;

            private void Button_Click(object sender, RoutedEventArgs e)
             {
                 _sample1.FontSize = 30.0F;
                 _sample2.FontSize = 75.0F;
             }
         }
         public sealed class SampleClass : INotifyPropertyChanged
         {
             public SampleClass(string message,float fontSize)
             {
                 _message = message;
                 _fontSize = fontSize;
             }
             public event PropertyChangedEventHandler PropertyChanged;
             private void OnPropertyChanged(string propertyName)
             {
                 if (PropertyChanged != null)
                 {
                     PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                 }
             }
             private string _message;
             public string Message
             {
                 get { return _message; }
                 set
                 {
                     _message = value;
                     OnPropertyChanged("Message");
                 }
             }
             private float _fontSize;
             public float FontSize
             {
                 get { return _fontSize; }
                 set
                 {
                     _fontSize = value;
                     OnPropertyChanged("FontSize");
                 }
             }
         }
     }


     [XAML]

    <Window x:Class="BindingTest.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"       
             x:Name="MainWin" Title="MainWindow" Height="350" Width="525">
         <StackPanel>
             <Button Content="Button" Click="Button_Click"/>
             <!-- Textには_sample1インスタンスのMessageプロパティ、FontSizeには_sample2インスタンスのFontSizeプロパティ、をバインドしたい。-->
             <!-- 複数のCLRインスタンスを同時に参照する方法が知りたい。 -->
             <TextBox Height="100" TextWrapping="Wrap" Text="{Binding Message}" FontSize="{Binding FontSize}" Background="LightBlue"/>
         </StackPanel>
     </Window>


    C#開発者

    2014年10月17日 5:21

回答

  • バインドされないのは trapemiya さんのご指摘どおりです。さらにざっとコードを見させていただきますと、

    > this.DataContext = this;

    が何とも気持ち悪いコードになってしまってます。よほどのことがない限り、Window.DataContext に自身のインスタンスを割り当てることは致しません。
    先の回答でも申しましたが、この程度のサンプルなら ViewModel を用意すればコードビハインドに一行も実装しないてバインドが可能になります。幾つか仕掛け(コマンドの用意等)が必要になりますが、ざっと実装すると以下のようになります。

    まず ICommand インターフェイスを実装したクラスを用意します。ネット上にサンプルがよく転がってる RelayCommand クラスです。

    using System;
    using System.Windows.Input;
    
    namespace BindingTest {
        /// <summary>
        /// コマンドバインド用クラス
        /// </summary>
        public sealed class RelayCommand : ICommand {
    
            public event EventHandler CanExecuteChanged;
            private Func<bool> _canExecuteAction;
            private Action _executeAction;
    
            public Action ExecuteAction {
                get { return _executeAction; }
                set { _executeAction = value; }
            }
    
            public Func<bool> CanExecuteFunc {
                get { return _canExecuteAction; }
                set { _canExecuteAction = value; }
            }
    
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public RelayCommand(Action executeAction) {
                this._executeAction = executeAction;
                this._canExecuteAction = () => true;
            }
    
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public RelayCommand(Action executeAction, Func<bool> canExecuteAction) {
                this._executeAction = executeAction;
                this._canExecuteAction = canExecuteAction;
            }
    
            /// <summary>
            /// 現在の状態でこの RelayCommand を実行できるかどうかを判断します。
            /// </summary>
            public bool CanExecute(object parameter) {
                var ret = _canExecuteAction();
                //UIHelpers.InvokePriority();
                return ret;
            }
    
            /// <summary>
            /// 現在のコマンドの対象で RelayCommand を実行します。
            /// </summary>
            public void Execute(object parameter) {
                if (_executeAction != null) {
                    try {
                        _executeAction();
                    } catch (Exception) {
                        throw;
                    }
                }
            }
    
            public void RaiseCanExecuteChanged() {
                if (CanExecuteChanged != null) {
                    CanExecuteChanged(this, EventArgs.Empty);
                }
            }
        }
    }

    次に ViewModel を用意します。

    using System.ComponentModel;
    
    namespace BindingTest {
        class ViewModel : INotifyPropertyChanged {
            public ViewModel() {
                this.SampleClass = new SampleClass("Sample1 Start!", 20.0F);
            }
    
            #region SampleClass変更通知プロパティ
            private SampleClass _SampleClass;
    
            public SampleClass SampleClass {
                get { return _SampleClass; }
                set { 
                    if (_SampleClass == value)
                        return;
                    _SampleClass = value;
                    OnPropertyChanged("SampleClass");
                }
            }
            #endregion
    
            #region コマンドの実装
            private RelayCommand _changeMessageCommand;
            public RelayCommand ChangeMessageCommand {
                get { return _changeMessageCommand = _changeMessageCommand ?? new RelayCommand(ChangeSample); }
            }
    
            private void ChangeSample() {
                this.SampleClass = new SampleClass("Sample2 Click!", 100.0F);
            }
            #endregion
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void OnPropertyChanged(string propertyName) {
                if (PropertyChanged != null) {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }

    ViewModel に View とのバインドを委ねたので、SampleClass の実装はだいぶ軽くなりました。

    using System;
    
    namespace BindingTest {
        public sealed class SampleClass {
            public SampleClass(string message, float fontSize) {
                Message = message;
                FontSize = fontSize;
            }
            public string Message { get; set; }
            public float FontSize { get; set; }
        }
    }

    最後は View である XAML です。コードビハインドを実装せず XAMLだけでバインドが可能になりました。

    <Window x:Class="BindingTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:BindingTest"
            x:Name="MainWin" Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:ViewModel />
        </Window.DataContext>
        <StackPanel>
            <Button Content="Button" Command="{Binding ChangeMessageCommand, Mode=OneWay}"/>
            <TextBox x:Name="TextBox1" Height="200" TextWrapping="Wrap" 
                     Text="{Binding SampleClass.Message}" 
                     FontSize="{Binding SampleClass.FontSize}" Background="LightBlue"/>
        </StackPanel>
    </Window>

    以上、何かの参考にあれば幸いです。


    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx


    2014年10月20日 9:26
    モデレータ

すべての返信

  • DataContextに_sample1、_sample2が含まれるように、MainWindowクラスのインスタンスそのものを渡します。
    _sample1、_sample2がバインドできるように、プロパティとして定義します。
    Xamlにおけるバインディングは、_sample1.Messageのように書けます。

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
             _sample1 = new SampleClass("Sample1",20.0F);
             _sample2 = new SampleClass("Sample2",50.0F);
             //this.DataContext = _sample1;
            this.DataContext = this;
        }
    
        //private SampleClass _sample1;
        //private SampleClass _sample2;
    
        public SampleClass _sample1{get; set;}
        public SampleClass _sample2 { get; set; }
    
        private void Button_Click(object sender, RoutedEventArgs e)
         {
             //_sample1.FontSize = 30.0F;
             _sample2.FontSize = 75.0F;
         }
     }
     
     <TextBox Height="100" TextWrapping="Wrap" Text="{Binding _sample1.Message}" 
             FontSize="{Binding _sample2.FontSize}" Background="LightBlue"/> 
    



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


    2014年10月17日 5:32
    モデレータ
  • CLRインスタンスを直接バインドすることがどうしてもできない場合は、何かを仲介して間接的に参照するアイデアでも構いませんが、それでも、UIとソースの分離というWPFの目標は維持したいし、複雑化によるコードの可読性の低下も最小限度に抑えたいと思っています。

    端的に言わせて頂くなら、ViewModel を用意すればいいです。 今回の場合、View のデータストアとして ViewModel を用意すれば、ほぼ問題は解決すると思われます。


    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx

    2014年10月17日 5:35
    モデレータ
  • _sample1と_sample2をResourcesに入れてBindingのSourceにStaticResourceで設定する方法も使えますね。

    _sample1 = new SampleClass();
    _sample2 = new SampleClass();
    
    this.Resources["_sample1"] = _sample1;
    this.Resources["_sample2"] = _sample2;

     <TextBox Height="100" TextWrapping="Wrap" Text="{Binding Message, Source={StaticResource _sample1}}" FontSize="{Binding FontSize, Source={StaticResource _sample1}}" Background="LightBlue"/>


    かずき Blog:http://d.hatena.ne.jp/okazuki/

    2014年10月18日 12:27
  • ご回答ありがとうございます。
    下記のようにソースを修正してみましたが、
    TextBox1には何も表示されませんでした。
    どこが間違っているのでしょうか?

    [C#]

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Markup;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace BindingTest
    {
        /// <summary>
        /// MainWindow.xaml の相互作用ロジック
        /// </summary>
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                _sample1 = new SampleClass("Sample1 Start!",20.0F);
                _sample2 = new SampleClass("Sample2 Start!",50.0F);
                this.DataContext = this;
            }
    
            /// <summary>
            /// サンプル1インスタンス
            /// </summary>
            public SampleClass _sample1 { get; set; }
    
            /// <summary>
            /// サンプル2インスタンス
            /// </summary>
            public SampleClass _sample2 { get; set; }
    
    
            private void Button_Click(object sender, RoutedEventArgs e)
            {
                _sample1.FontSize = 40.0F;
                _sample1.Message = "Sample1 Click!";
                _sample2.FontSize = 100.0F;
                _sample2.Message = "Sample2 Click!";
            }
        }
    
        public sealed class SampleClass : INotifyPropertyChanged
        {
            public SampleClass(string message,float fontSize)
            {
                _message = message;
                _fontSize = fontSize;
                OnPropertyChanged("Message");
                OnPropertyChanged("FontSize");
            }
            public event PropertyChangedEventHandler PropertyChanged;
            private void OnPropertyChanged(string propertyName)
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
            private string _message;
            public string Message 
            {
                get { return _message; }
                set
                {
                    _message = value;
                    OnPropertyChanged("Message");
                }
            }
            private float _fontSize;
            public float FontSize
            {
                get { return _fontSize; }
                set
                {
                    _fontSize = value;
                    OnPropertyChanged("FontSize");
                }
            }
        }
    }

    [XAML]

    <Window x:Class="BindingTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        
            x:Name="MainWin" Title="MainWindow" Height="350" Width="525">
        <StackPanel>
            <Button Content="Button" Click="Button_Click"/>
            <!-- Textには_sample1インスタンスのMessageプロパティ、FontSizeには_sample2インスタンスのFontSizeプロパティ、をバインドしたい。-->
            <!-- 複数のCLRインスタンスを同時に参照する方法が知りたい。 -->
            <TextBox x:Name="TextBox1" Height="200" TextWrapping="Wrap" Text="{Binding _source1.Message}" FontSize="{Binding _source2.FontSize}" Background="LightBlue"/>
        </StackPanel>
    </Window>


    C#開発者

    2014年10月20日 7:49
  • XAMLのBindingのところで、_source1, _source2となっていますが、_sample1, _sample2の間違いですね。

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

    2014年10月20日 8:24
    モデレータ
  • バインドされないのは trapemiya さんのご指摘どおりです。さらにざっとコードを見させていただきますと、

    > this.DataContext = this;

    が何とも気持ち悪いコードになってしまってます。よほどのことがない限り、Window.DataContext に自身のインスタンスを割り当てることは致しません。
    先の回答でも申しましたが、この程度のサンプルなら ViewModel を用意すればコードビハインドに一行も実装しないてバインドが可能になります。幾つか仕掛け(コマンドの用意等)が必要になりますが、ざっと実装すると以下のようになります。

    まず ICommand インターフェイスを実装したクラスを用意します。ネット上にサンプルがよく転がってる RelayCommand クラスです。

    using System;
    using System.Windows.Input;
    
    namespace BindingTest {
        /// <summary>
        /// コマンドバインド用クラス
        /// </summary>
        public sealed class RelayCommand : ICommand {
    
            public event EventHandler CanExecuteChanged;
            private Func<bool> _canExecuteAction;
            private Action _executeAction;
    
            public Action ExecuteAction {
                get { return _executeAction; }
                set { _executeAction = value; }
            }
    
            public Func<bool> CanExecuteFunc {
                get { return _canExecuteAction; }
                set { _canExecuteAction = value; }
            }
    
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public RelayCommand(Action executeAction) {
                this._executeAction = executeAction;
                this._canExecuteAction = () => true;
            }
    
            /// <summary>
            /// コンストラクタ
            /// </summary>
            public RelayCommand(Action executeAction, Func<bool> canExecuteAction) {
                this._executeAction = executeAction;
                this._canExecuteAction = canExecuteAction;
            }
    
            /// <summary>
            /// 現在の状態でこの RelayCommand を実行できるかどうかを判断します。
            /// </summary>
            public bool CanExecute(object parameter) {
                var ret = _canExecuteAction();
                //UIHelpers.InvokePriority();
                return ret;
            }
    
            /// <summary>
            /// 現在のコマンドの対象で RelayCommand を実行します。
            /// </summary>
            public void Execute(object parameter) {
                if (_executeAction != null) {
                    try {
                        _executeAction();
                    } catch (Exception) {
                        throw;
                    }
                }
            }
    
            public void RaiseCanExecuteChanged() {
                if (CanExecuteChanged != null) {
                    CanExecuteChanged(this, EventArgs.Empty);
                }
            }
        }
    }

    次に ViewModel を用意します。

    using System.ComponentModel;
    
    namespace BindingTest {
        class ViewModel : INotifyPropertyChanged {
            public ViewModel() {
                this.SampleClass = new SampleClass("Sample1 Start!", 20.0F);
            }
    
            #region SampleClass変更通知プロパティ
            private SampleClass _SampleClass;
    
            public SampleClass SampleClass {
                get { return _SampleClass; }
                set { 
                    if (_SampleClass == value)
                        return;
                    _SampleClass = value;
                    OnPropertyChanged("SampleClass");
                }
            }
            #endregion
    
            #region コマンドの実装
            private RelayCommand _changeMessageCommand;
            public RelayCommand ChangeMessageCommand {
                get { return _changeMessageCommand = _changeMessageCommand ?? new RelayCommand(ChangeSample); }
            }
    
            private void ChangeSample() {
                this.SampleClass = new SampleClass("Sample2 Click!", 100.0F);
            }
            #endregion
    
            public event PropertyChangedEventHandler PropertyChanged;
            private void OnPropertyChanged(string propertyName) {
                if (PropertyChanged != null) {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }

    ViewModel に View とのバインドを委ねたので、SampleClass の実装はだいぶ軽くなりました。

    using System;
    
    namespace BindingTest {
        public sealed class SampleClass {
            public SampleClass(string message, float fontSize) {
                Message = message;
                FontSize = fontSize;
            }
            public string Message { get; set; }
            public float FontSize { get; set; }
        }
    }

    最後は View である XAML です。コードビハインドを実装せず XAMLだけでバインドが可能になりました。

    <Window x:Class="BindingTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:BindingTest"
            x:Name="MainWin" Title="MainWindow" Height="350" Width="525">
        <Window.DataContext>
            <local:ViewModel />
        </Window.DataContext>
        <StackPanel>
            <Button Content="Button" Command="{Binding ChangeMessageCommand, Mode=OneWay}"/>
            <TextBox x:Name="TextBox1" Height="200" TextWrapping="Wrap" 
                     Text="{Binding SampleClass.Message}" 
                     FontSize="{Binding SampleClass.FontSize}" Background="LightBlue"/>
        </StackPanel>
    </Window>

    以上、何かの参考にあれば幸いです。


    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx


    2014年10月20日 9:26
    モデレータ
  • ご回答ありがとうございます。

    SampleClassとXAMLがすっきり記述できているので驚きました。

    私はViewModelを知りませんでしたので、興味を持ちました。


    C#開発者

    2014年10月23日 9:07