【WPF】FrameworkElement.DataContextプロパティの値が変わるタイミング。

  • 2014.03.30 Sunday
  • 14:23
JUGEMテーマ:コンピュータ

【WPF】FrameworkElement.DataContextプロパティを無効にする方法
http://pro.art55.jp/?eid=1304213


前回は、DataContextに設定した値をクリアーし、親要素の値を再設定したい場合は、DependencyProperty.UnsetValueを設定するばよいという方法を紹介しましたが、今回はDataContextの値が変更されるタイミングを見ていきたいと思います。私の個人の意見としては、一度設定したDataContextの値を変えてしまうような実装は、WPFアプリケーションの設計としては、バグの元になりやすく、間違っていると思います。(伝搬して変更した子要素のDataContextも値の書き換えという事にはなりますが、それは対象外と・・・)

今回は、DataContextの値が変わるタイミングです。正直、私はこの点に関して意識したことは全くありませんでした。前述の通り書き換えないからです。必要なタイミングまでには、値が設定されていたので気にしていなかったということなのですが、DependencyProperty.UnsetValueを設定したタイミングで、その設定した要素や親要素を見に行っているような動きをしていたため、少し違和感を感じたのがきっかけです。親要素が変わったタイミングで子要素に伝搬するだけだと思っていたからです。

試してみました。

    <Grid Background="Pink" x:Name="TopPanel" DataContextChanged="OnDataContextChanged">
        <Grid Margin="40" Background="HotPink" x:Name="MiddlePanel" DataContextChanged="OnDataContextChanged">
            <Grid Margin="40" x:Name="BottomPanel"  Background="DeepPink" DataContextChanged="OnDataContextChanged">
                <Button Margin="40" x:Name="Button" Click="OnClick"  DataContextChanged="OnDataContextChanged">Click</Button>
            </Grid>
        </Grid>
    </Grid>

        private void OnClick(object sender, RoutedEventArgs e)
        {
            Debug.Print("BottomPanel.DataContext に20を設定する");
            BottomPanel.DataContext = 20;

            Debug.Print("TopPanel.DataContextに10を設定する");
            TopPanel.DataContext = 10;

            Debug.Print("BottomPanel.DataContextにDependencyProperty.UnsetValueを設定する");
            BottomPanel.DataContext = DependencyProperty.UnsetValue;

            Debug.Print("Buttonのコンテンツを書き換える。");
            var button = (Button)sender;
            button.Content = button.DataContext;
        }

        private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            var element = sender as FrameworkElement;
            string message = string.Format("{0}.{1} = {2}", element.Name, e.Property.Name, e.NewValue);
            // MessageBox.Show(message);
            Debug.Print(message);
        }

これを実行すると、以下の出力が得られます。

BottomPanel.DataContext に20を設定する
BottomPanel.DataContext = 20
Button.DataContext = 20
TopPanel.DataContextに10を設定する
TopPanel.DataContext = 10
MiddlePanel.DataContext = 10
BottomPanel.DataContextにDependencyProperty.UnsetValueを設定する
BottomPanel.DataContext = 10
Button.DataContext = 10
Buttonのコンテンツを書き換える。


VirualTreeの位置関係はXAMLの通り

TopPanel - MiddlePanel - BottomPanel - Button

まず、BottomPanelの値を変えると、
BottomPanelとその子のButtonのDataContextの値が変わっていることがわかります。

TopPanel - MiddlePanel - BottomPanel(20) - Button(20)

TopPanelの値を変えると、TopPanel とMiddlePanel まで値が変わります。
すでにBottomPanelに値が設定されているため、BottomPanel以降は伝搬しません。

TopPanel(10) - MiddlePanel(10) - BottomPanel(20) - Button(20)

BottomPanelをDependencyProperty.UnsetValueを設定することで、無効にします。
そうすると、そのタイミングで、BottomPanelは親要素のDataContextが反映されます。
そして、子要素(Button)にも伝搬します。

TopPanel(10) - MiddlePanel(10) - BottomPanel(無効) - Button(20)
TopPanel(10) - MiddlePanel(10) - BottomPanel(10) - Button(10)

以上のことから、DataContextはDataContextに値を設定したタイミングで、常に自分自身から子要素までの値が切り替わっていることが確認できました。私が想定したもう一つの可能性は、DataContextの値が必要となったタイミングで値が切り替わっているのではないかというものがあったのですが、そうではないようです。「見込生産」か「受注生産」かどちらなんだろうってちょいと思ったんですが、「見込生産」に該当するようです。

まとめ、

1.FrameworkElement.DataContextプロパティは、値が変わったタイミングで、
  値が設定されていない直近の子要素まで伝搬する。
2.FrameworkElement.DataContextプロパティに、DependencyProperty.UnsetValueを設定すると
  設定された要素は、親要素から値を取得し設定される(値の場合は値、nullの場合はnull)。
  さらに1の通り、値が伝搬する。

 

【WPF】FrameworkElement.DataContextプロパティを無効にする方法

  • 2014.03.30 Sunday
  • 05:42
JUGEMテーマ:コンピュータ
 
値を代入したFrameworkElement.DataContextプロパティに対して、親要素に存在するFrameworkElement.DataContextプロパティの値を再取得したい場合は、nullを指定するのではなく、DependencyProperty.UnsetValueを指定する必要がある場合があります。

例えば、以下のコードが書かれていたとします。

    <Grid Background="Pink" x:Name="TopPanel">
        <Grid Margin="40" Background="HotPink" x:Name="MiddlePanel">
            <Grid Margin="40" Background="DeepPink">
                <Button Margin="40" Click="OnClick">Click</Button>
            </Grid>
        </Grid>
    </Grid>

        private void OnClick(object sender, RoutedEventArgs e)
        {
            MiddlePanel.DataContext = 20;
            TopPanel.DataContext = 10;
            MiddlePanel.DataContext = DependencyProperty.UnsetValue;

            var button = (Button) sender;
            button.Content = button.DataContext;
        }

上記のサンプルコードでは
buttonのContentプロパティに10が代入されます。

MiddlePanel.DataContext = DependencyProperty.UnsetValue;

の部分を

MiddlePanel.DataContext = null;

と書くと、Contentはnullが代入されます(nullが代入されるという言い回しはおかしい気がしますが)。

DataContextプロパティに限った話ではありませんが、nullを指定するのではなくDependencyProperty.UnsetValueを代入する必要があります。それが理由かどうかは正確なところはわかりませんが、DependencyProperty.UnsetValueを代入したいがために依存関係プロパティの型がobject型になってしまっているところがあります。ItemsSourceの型がobject型になっているのは多分そのせいかな・・・。

------------------
(参考)

クラス設計から見たDataContextプロパティ
DataContextプロパティは、FrameworkElementクラスで定義されているプロパティ
DataContextプロパティは、依存関係プロパティ。
DataContextプロパティは、FrameworkPropertyMetadataOptions.Inheritsオプションが指定されている。

参考資料
FrameworkElement.DataContext プロパティ
http://msdn.microsoft.com/ja-jp/library/system.windows.frameworkelement.datacontext(v=vs.110).aspx


------------------
(追記)
「ItemsSourceの型」に関してちょいと意味不明なのでした。

【WPF】DataGrid.AreAnyTouchesCapturedWithinを使ってみた。

  • 2014.03.13 Thursday
  • 22:22
JUGEMテーマ:コンピュータ

Source and Project

MSDNに記載されているWPF/DataGridのプロパティを眺めています。よくわからないものはサンプルコードを書いてみて動きを調べたりしています。本日は、その調べた中でも「あれ?」って思ったプロパティの一つ「DataGrid.AreAnyTouchesCapturedWithin」を紹介いたします。

DataGrid.AreAnyTouchesCapturedWithinはUIElementから継承するプロパティです。名前から推測するにタッチ操作関連のプロパティだとすぐにわかりますが、私は最初、依存関係プロパティなのでBindingオブジェクトを通じて、タッチの有無をトリガーとする機能を実装するものだと思ったんですね。

<DataGrid AreAnyTouchesCapturedWithin={Binding ... Mode=OneWayToSource} ...>

こんな記述ができるものだと思ったのですが、これはコンパイルエラーです。setterがないからエラーとコンパイラが教えてくれました。うーん。依存関係プロパティなのにできないんですが・・・。

で、まあ、依存関係プロパティであることは変わりないのでDependencyPropertyDescriptorを利用して、値の変更通知(実際には変更されたことは、わかりますが変更値の通知は来ません)をごにょごにょするコードを書いてみました。

<Window x:Class="Art55.DataGridAreAnyTouchesCapturedWithin20140313_001.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"
        DataContext="{StaticResource MainWindowViewModelKey}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="*" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="30" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="30" />
        </Grid.ColumnDefinitions>
       
        <DataGrid Grid.Row="1" Grid.Column="1"
                  x:Name="dataGrid"
                  ItemsSource="{Binding .Source.DefaultView}"></DataGrid>
       
     </Grid>
</Window>

using System;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Controls;
using System.Windows.Media;
using Art55.DataGridAreAnyTouchesCapturedWithin20140313_001.Annotations;

namespace Art55.DataGridAreAnyTouchesCapturedWithin20140313_001
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            var dependency = DependencyPropertyDescriptor
                .FromProperty(AreAnyTouchesCapturedWithinProperty, typeof (DataGrid));
            dependency.AddValueChanged(dataGrid, OnAreAnyTouchesCapturedWithin);

        }

        private void OnAreAnyTouchesCapturedWithin(object sender, EventArgs eventArgs)
        {
            Background = dataGrid.AreAnyTouchesCapturedWithin
                ? Brushes.HotPink
                : Brushes.White;
        }

    }

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
            _source = new DataTable {CaseSensitive = true};
            _source.Columns.Add("Columns1");
            _source.Columns.Add("Columns2");
            _source.Columns.Add("Columns3");
            _source.Columns.Add("Columns4");
           
            Enumerable.Range(0, 10)
                .Select(n => new object[] {"A" + n, "B" + n, "C" + n, "D" + n})
                .ToList()
                .ForEach(item => _source.Rows.Add(item));

            _source.AcceptChanges();
        }

        private readonly DataTable _source;

        public DataTable Source
        {
            get { return _source; }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

赤で書いたコードの解説ですが、DataGridコントロール、またはその中のVisual Tree上に存在するコントロールがタッチされたら、メインウィンドウのBackgroundをピンクにするコードを記述しました。それ以外は白です。

実行するとタッチの最中はピンク色で離すと白に戻ります。

まあ、普通はタッチイベントが別途用意されており、タッチしたタイミング、タッチでジェスチャーをしている最中、タッチが外れたタイミングでそれぞれイベントが用意されているので、わざわざDependencyPropertyDescriptorを利用することはないと思いますが、プロパティ自体の動きが気になったので試してみました。おそらくは、本来は、コントロール側で利用するためのプロパティか、タッチ中のイベントハンドリングの最中にタッチ状況を判定するような使い方を想定したプロパティではないかと思います。私が作成したコントロールではないので、あくまで想像ですと付け足しておきます。想像です。

そうそう注意点として、AreAnyTouchesCapturedWithinとは別にAreAnyTouchesCapturedというプロパティが存在します。名前から推測される通り、前者はVisual Tree上の子要素も含める判定するのに対して、後者は子要素は含めません。DataGridのような複数のコントロールが無数にある状況では、AreAnyTouchesCapturedがtrueになるような状況は皆無ではないかと思います。名前が似ているので注意する必要があります。

まとめ

1.AreAnyTouchesCapturedWithinはBindingできない。XAML上で記述できない。
2.AreAnyTouchesCapturedWithinは、そのタイミングでのタッチのキャプチャ(指定したコントロール内に一つでもあればtrue)を判定する。
3.AreAnyTouchesCapturedWithinとAreAnyTouchesCapturedは似ているので注意。
4.今回のサンプルコードは役に立たない。タッチイベントを拾ったほうが良い。

以上です。

Source and Project

【WPF】手入力不可能なComboBoxに初期値を表示するには?【修正版】

  • 2014.03.11 Tuesday
  • 23:01
JUGEMテーマ:コンピュータ

Source and Project

.NET Framework 4.5.1を利用して、DataGrid中のセルに手入力可能なComboBoxを表示させる方法を先週紹介しましたが、問題があることが分かったので、今回は修正版のソースコードを紹介したいと思います。

以前紹介した投稿は以下となります。
【WPF】DataGridに編集可能なComboBoxを表示するには?

問題点の指摘は以下の投稿となります。
【WPF】「手入力不可能なComboBoxに初期値を表示するには?」の問題点

問題点は、挙動のおかしさを指摘したにとどまっていますが、実装・設計面から見てもStyleの定義でテーブルのカラム名を指定しているなど、ちょっと汎用性に欠ける点も問題点といえば問題点ではないかと思います。今回はそれを含めて解決してみました。

ソースコードを紹介します。

<Window x:Class="Art55.DataGridComboBoxDemo20140311_01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:demo="clr-namespace:Art55.DataGridComboBoxDemo20140311_01"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{StaticResource MainWindowViewModelKey}">
    <Window.Resources>
        <demo:FromTextToItemsSourceConvter x:Key="FromTextToItemsSourceConvterKey" />
        <demo:FromItemsSourceToSelectedIndexConverter x:Key="FromItemsSourceToSelectedIndexConverterKey" />

        <Style TargetType="ComboBox" x:Key="EditingElementStyleKey">
            <Setter Property="IsEditable" Value="True" />
        </Style>

        <Style TargetType="ComboBox" x:Key="ElementStyleKey">
            <Setter Property="ItemsSource"
                    Value="{Binding Path=Text,
                                    RelativeSource={RelativeSource Self},
                                    Converter={StaticResource FromTextToItemsSourceConvterKey}}" />
            <Setter Property="SelectedIndex"
                    Value="{Binding Path=ItemsSource,
                                    RelativeSource={RelativeSource Self},
                                    Converter={StaticResource FromItemsSourceToSelectedIndexConverterKey},
                                    Mode=OneWay}"></Setter>
        </Style>

    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid CanUserAddRows="False"
                  CanUserDeleteRows="False"
                  AutoGenerateColumns="False"
                  DataContext="{Binding Path=Source}"
                  ItemsSource="{Binding .}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="No" Binding="{Binding No}"></DataGridTextColumn>
                <DataGridComboBoxColumn Header="ComboBox Column"
                                        TextBinding="{Binding Value, UpdateSourceTrigger=PropertyChanged}"
                                        EditingElementStyle="{StaticResource EditingElementStyleKey}"
                                        ElementStyle="{StaticResource ElementStyleKey}">

                </DataGridComboBoxColumn>
            </DataGrid.Columns>
        </DataGrid>

        <Button Grid.Row="1" Command="{Binding RejectChangedAllRowsCommand}">変更取消</Button>
    </Grid>
</Window>

using System;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using Art55.DataGridComboBoxDemo20140311_01.Annotations;

namespace Art55.DataGridComboBoxDemo20140311_01
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
            _source = new DataTable {CaseSensitive = true};
            _source.Columns.Add("No", typeof(int));
            _source.Columns.Add("Value");
            _source.Rows.Add(new object[] { 1, DBNull.Value });
            _source.Rows.Add(new object[] { 2, "A2" });
            _source.Rows.Add(new object[] { 3, "A3" });
            _source.Rows.Add(new object[] { 4, DBNull.Value });
            _source.Rows.Add(new object[] { 5, DBNull.Value });
            _source.Rows.Add(new object[] { 6, "A6" });
            _source.Rows.Add(new object[] { 7, "A7" });
            _source.Rows.Add(new object[] { 8, DBNull.Value });

            _source.AcceptChanges();

            RejectChangedAllRowsCommand = new CommandObject(this,
                _ => _source.RejectChanges());
        }

        public ICommand RejectChangedAllRowsCommand { get; private set; }

        // TODO コマンドを用意してDataTableを操作してみると面白いよ。

        private readonly DataTable _source;

        public DataTable Source
        {
            get { return _source; }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public class FromTextToItemsSourceConvter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null || value == DependencyProperty.UnsetValue)
            {
                return DependencyProperty.UnsetValue;
            }
            return new[] {value};
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

    public class FromItemsSourceToSelectedIndexConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var items = value as object[];
            return items == null ? -1 : 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }


    public class CommandObject : ICommand
    {
        public CommandObject(INotifyPropertyChanged viewModel, Action<object> execute)
            : this(viewModel, execute, null)
        {
        }

        public CommandObject(INotifyPropertyChanged viewModel, Action<object> execute, Func<object, bool> canExecute)
        {
            if (viewModel == null)
                throw new ArgumentNullException("viewModel");

            if (execute == null)
                throw new ArgumentNullException("execute");

            viewModel.PropertyChanged += OnViewModelPropertyChanged;
            _execute = execute;
            _canExecute = canExecute;
        }

        void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            CheckCanExecute();
        }

        public void CheckCanExecute()
        {
            InvokeCanExecuteChanged(new EventArgs());
        }

        public void Execute(object parameter)
        {
            _execute.Invoke(parameter);
        }

        public bool CanExecute(object parameter)
        {
            if (_canExecute != null)
                return _canExecute.Invoke(parameter);
            return true;
        }

        public event EventHandler CanExecuteChanged;

        private void InvokeCanExecuteChanged(EventArgs e)
        {
            EventHandler changed = CanExecuteChanged;
            if (changed != null)
                changed(this, e);
        }

        private readonly Action<object> _execute;
        private readonly Func<object, bool> _canExecute;
    }
}

ポイントは、赤字で書いた部分です。

        <Style TargetType="ComboBox" x:Key="EditingElementStyleKey">
            <Setter Property="IsEditable" Value="True" />
        </Style>

        <Style TargetType="ComboBox" x:Key="ElementStyleKey">
            <Setter Property="ItemsSource"
                    Value="{Binding Path=Text,
                                    RelativeSource={RelativeSource Self},
                                    Converter={StaticResource FromTextToItemsSourceConvterKey}}" />
            <Setter Property="SelectedIndex"
                    Value="{Binding Path=ItemsSource,
                                    RelativeSource={RelativeSource Self},
                                    Converter={StaticResource FromItemsSourceToSelectedIndexConverterKey},
                                    Mode=OneWay}"></Setter>
        </Style>


1.編集中は、ComboBox.IsEditableにTrueを指定する。
2.参照中は、ComboBox.Textの変更を監視し、変更のあったタイミングでItemsSourceに入力文字を設定する。
3.参照中は、ComboBox.ItemsSourceを監視し、ComboBox.ItemsSourceの数に応じてSelectedIndexを設定する。

監視対処が

Text ← ItemsSource ← SelectedIndex

と、XAMLで定義しました。以前書いたコードは、DataRowの特定のカラム値やDataRowViewの変更を監視していたりと、タイミングに不整合が起きていたのですが、今回は、それを解消してみました。

 手入力可能なComboBoxを表示させる方法はいくつかあります。前に書いた方法としてDataGridTemplateColumnを利用するというのも一つの手です。WpfToolkit時代はDataGridComboBoxColumnは編集時はComboBox、参照時はTextBlockを利用していたと記憶していますが、DataGridTemplateColumnを利用すれば、近いものが実装できると思います。
 それと、今回の実装では、まだ機能的にはお粗末です。すぐにわかるのが、一つは入力開始制御がF2とダブルクリック以外でできていない点です。ほかにもあるかもしれません。という事で、ないと思いますが、このコードを利用する場合は注意してください。

Source and Project

【WPF】「手入力不可能なComboBoxに初期値を表示するには?」の問題点

  • 2014.03.08 Saturday
  • 07:41
JUGEMテーマ:コンピュータ

【WPF】手入力不可能なComboBoxに初期値を表示するには?
http://pro.art55.jp/?eid=1304202


上記の記事でDataGrid中に編集可能なComboBoxを表示させる方法を紹介しましたが、一部問題があることが分かったので、今回は、問題点を考察したいと思います。解決案は別途投稿する予定です。

では、問題点をあげます。

・操作によって参照モードで値があるにも関わらず、値が表示されないことがある。

ということが、わかりました。操作方法は以下の通り。

1.適当にセルを編集する。
2.編集したセルが見えないようにDataGridをスクロールする。
3.編集したセルの値をもとに戻す(DataRow.RejectChangedメソッドを呼び出す)
4.もとに戻したセルを表示させ、もう一度編集する。
5.編集したセルを表示させた状態で、セルの値をもとに戻す。

そうすると、編集したセルに値が空になります。この問題がどうして発生しているかというのが今回の本題となります。RejectChangedを呼び出す処理が入った状態のコードを以下に紹介します。

 

<Window x:Class="Art55.EditableComboBoxDataGrid20140308_02.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:editableComboBoxDataGrid2014030802="clr-namespace:Art55.EditableComboBoxDataGrid20140308_02"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>

        <editableComboBoxDataGrid2014030802:TextBlockComboBoxItemsSourceConverter x:Key="TextBlockComboBoxItemsSourceConverterKey" />

        <!-- 編集モード時のComboBox -->
        <Style TargetType="ComboBox" x:Key="EditingElementStyle">
            <Setter Property="IsEditable" Value="True" />
        </Style>

        <!-- 参照モード時のComboBox -->
        <Style TargetType="ComboBox" x:Key="TextBlockComboBoxStyle">
            <!--<Setter Property="Text" Value="{Binding Column1}" />-->
            <Setter Property="ItemsSource"
                     Value="{Binding .,
                             Converter={StaticResource TextBlockComboBoxItemsSourceConverterKey},
                             ConverterParameter=Column1}" />

        </Style>

        <!-- 既定のDataGridのスタイル -->
        <Style TargetType="DataGrid">
            <Setter Property="AutoGenerateColumns" Value="False" />
            <Setter Property="CanUserAddRows" Value="False" />
            <Setter Property="CanUserDeleteRows" Value="False" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="SampleDataGrid">
            <DataGrid.Columns>
                <DataGridComboBoxColumn Header="Column1"
                                        TextBinding="{Binding Column1, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                         EditingElementStyle="{StaticResource EditingElementStyle}"
                                         ElementStyle="{StaticResource TextBlockComboBoxStyle}"
                                         />
            </DataGrid.Columns>
        </DataGrid>

        <Button Grid.Row="1" Click="OnSelectedRowsRejectChanges">選択行の値を元に戻す。</Button>
    </Grid>
</Window>

using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;

namespace Art55.EditableComboBoxDataGrid20140308_02
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var dataTable = new DataTable { TableName = "SampleData" };
            dataTable.Columns.Add("Column1");
            dataTable.Columns.Add("Column2");
            dataTable.Columns.Add("Column3");

            Enumerable.Range(0, 300)
                 .Select(n => new object[] { "A" + n, "B" + n, "C" + n })
                 .ToList()
                 .ForEach(item => dataTable.Rows.Add(item));

            dataTable.AcceptChanges();

            SampleDataGrid.ItemsSource = dataTable.DefaultView;
        }

        private void OnSelectedRowsRejectChanges(object sender, RoutedEventArgs e)
        {
            SampleDataGrid
                .SelectedItems
                .OfType<DataRowView>()
                .Select(rowView => rowView.Row)
                .Where(row => row != null)
                .Where(row => row.RowState == DataRowState.Modified)
                .ToList()
                .ForEach(row => row.RejectChanges());
        }
    }

    public class TextBlockComboBoxItemsSourceConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string columnName = (parameter as string) ?? string.Empty;
            var dataRowView = value as DataRowView;
            if (dataRowView == null
                || dataRowView.Row == null
                || !dataRowView.Row.Table.Columns.Contains(columnName))
            {
                return DependencyProperty.UnsetValue;
            }
            return new[] { dataRowView.Row[columnName] };
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

上記のコードのどこが問題かというとDataGridComboBoxColumn.ElementStyle用に用意したStyleです。

        <!-- 参照モード時のComboBox -->
        <Style TargetType="ComboBox" x:Key="TextBlockComboBoxStyle">
            <Setter Property="ItemsSource"
                     Value="{Binding .,
                             Converter={StaticResource TextBlockComboBoxItemsSourceConverterKey},
                             ConverterParameter=Column1}" />

        </Style>

ここのSetter部分ですが、DataRowViewを監視し、DataRowViewに変更があった際に、コンバートした値をComboBox.ItemsSourceプロパティに設定するというBindingを定義しています。問題なのはDataRowViewを監視しているという点です。DataRowView["Column1"]の値の変更を監視しているわけではないという点です。DataGridComboBox.TextBindingにより、編集中のComboBoxも編集中でないComboBoxもTextプロパティはDataRowView["Column1"]を監視しているのですが、DataRowView自体を監視しているわけではありません。よって、ComboBox.TextとComboBox.ItemsSourceの同期が取れていないという事になり、たとえばComboBox.Textには何らかの値があるにも関わらずComboBox.ItemsSourceに候補がないとか、ComboBox.ItemsSourceに値はあるけどComboBox.Textと不一致になるという問題が発生しうる事になります。

なので、今回見つけた問題が発生することになります。修正しなければならないのですが、今回の投稿は問題点を指摘するだけにとどめておきます。また、次回。

【WPF】DataGrid中の任意のセルを編集中にする。

  • 2014.03.06 Thursday
  • 01:24
今回は「DataGrid中の任意のセルを編集中にする。」方法の一案を紹介します。別にこの方法で必ず実現する必要なんてないんだからね!と、まあ、ツンデレ風な表現を用いたかっただけです。ごめんなさい。

DataGrid中の任意のセルを編集中にするには、前回紹介した

【WPF】DataGrid中の任意のセルにフォーカスを当てる。
http://pro.art55.jp/?eid=1304203


の手続きを踏んだ後、ひと手間加えるだけで、やりたいことが実現できます。手順は以下の通り。

1.フォーカスを当てたいDataGridInfoを生成または取得する。
2.DataGridInfoからセルのコンテンツを表示しているコントロールを取得する。
3.コンテンツを表示しているコントロールからDataGridCellコントロールyを取得する。
4.DataGridCellコントロールにフォーカスを当てる。
5.DataGridを編集開始にする。

5番目の手続きが増えただけです。5番目の手続きは、私が知る限りでは3つ方法があります。

A.
DataGrid.BeginEditメソッドを呼び出す。
B.DataGrid.BeginEditCommand.Executeメソッドを呼び出す。
C.DataGridCell.IsEditingプロパティにtrueを設定する。
Aはイベントドリブンな実装やマークアップ拡張の実装で利用できると思います。Bはコマンド用ですが、コードからでも呼び出せます。Cは編集中にするだけのシンプル状態変更です。編集中に切り替わるにあたり、編集開始の状態である場合に付随するフォーカス制御などはしてくれません。逆に言えば、編集中の初期状態を自前で制御したい場合は、Cを選択するべきです。AとBは実行すると、例えばDataGricTextBoxColumnを指定した場合は、編集中になった場合に、TextBox内のTextが全選択になりますし、先日紹介した手入力可能なDataGridComboBoxColumnを指定した場合もTextBox内のテキストが全選択上になります。便利です。

ということでサンプルコード。


<Window x:Class="Art55.DataGridCell20140305_001.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">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="dataGrid" />
        <Button Grid.Row="1" Click="OnClickick">40行2列目のセルを編集モードにする。</Button>
    </Grid>
</Window>
 

using System.Data;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Art55.DataGridCell20140305_001
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            DataTable source = CreateSampleData();
            dataGrid.ItemsSource = source.DefaultView;
        }

        private void OnClickick(object sender, RoutedEventArgs e)
        {
            var dataGridCellInfo = new DataGridCellInfo(dataGrid.Items[40], dataGrid.Columns[2]);
            dataGrid.CurrentCell = dataGridCellInfo;
            dataGrid.ScrollIntoView(dataGridCellInfo.Item);
            DoEvents();
            FrameworkElement contentElement = dataGridCellInfo.Column.GetCellContent(dataGridCellInfo.Item);
            if (contentElement == null)
            {
                return;
            }
            var dataGridCell = contentElement.Parent as DataGridCell;
            if (dataGridCell == null)
            {
                return;
            }
            dataGridCell.Focus();

            // IsEditingを利用した場合、コンテンツを表示しているコントロールにフォーカスが当たらないなどの諸問題がある。
            // dataGridCell.IsEditing = true;

            // DataGrid.BeginEditCommand.Execute(null, dataGridCell);

            dataGrid.BeginEdit();
        }

        private static void DoEvents()
        {
            // NOTE: http://msdn.microsoft.com/ja-jp/library/system.windows.threading.dispatcher.pushframe(VS.80).aspx
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrames), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrames(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            return null;
        }

        private static DataTable CreateSampleData()
        {
            var source = new DataTable();
            source.Columns.Add("Column1");
            source.Columns.Add("Column2");
            source.Columns.Add("Column3");
            source.Columns.Add("Column4");

            Enumerable.Range(0, 100)
                .GroupBy(n => Enumerable
                    .Range(0, source.Columns.Count)
                    .Select(m => ((char)('A' + m))
                        .ToString(CultureInfo.InvariantCulture) + n)
                    .Cast<object>()
                    .ToArray()
                    , (n, m) => n)
                .ToList()
                .ForEach(item => source.Rows.Add(item));

            source.AcceptChanges();
            return source;
        }
    }
}


まとめると

DataGridの任意のセルを編集モードにしたい場合は

1.セルにフォーカスを当てる。
2.
DataGrid.BeginEditメソッドを呼び出す。

です。フォーカスを当てるのが結構難があります。詳しくは以下を参照してください。
【WPF】DataGrid中の任意のセルにフォーカスを当てる。
http://pro.art55.jp/?eid=1304203

 

【WPF】DataGrid中の任意のセルにフォーカスを当てる。

  • 2014.03.06 Thursday
  • 00:43
今回は「DataGrid中の任意のセルにフォーカスを当てる。」方法を紹介します。
簡単な手順を紹介すると

1.フォーカスを当てたいDataGridInfoを生成または取得する。
2.DataGridInfoからセルのコンテンツを表示しているコントロールを取得する。
3.コンテンツを表示しているコントロールからDataGridCellコントロールyを取得する。
4.DataGridCellコントロールにフォーカスを当てる。
という手順となるわけですが、一言でいうとDataGridCellコントロールにフォーカスを当てるってことになります。手順を書き下しましたが、問題があります。「コンテンツを表示しているコントロール」や「DataGridCellコントロール」というのは書いてある通り「コントロール」なので設定によっては表示されていない状態だと、コントロールのインスタンスが存在しない事になります。うん、忘れてました。なので、今回はその回避方法も含めて紹介します。WPFToolkitを利用している際に、覚えてテクニックです(.NET Framework 4.5.1でもやらないといけないのだろうか・・・もっと賢くエレガントな方法ってあるのかな。)

さっきの手順を追記します。

1.フォーカスを当てたいDataGridInfoを生成または取得する。
+.DataGridInfoで指定したセル(DataGridCellコントロールおよびそのコンテンツ)のインスタンスを強引に生成する。
2.DataGridInfoからセルのコンテンツを表示しているコントロールを取得する。
3.コンテンツを表示しているコントロールからDataGridCellコントロールyを取得する。
4.DataGridCellコントロールにフォーカスを当てる。
 
これでできます。以下、サンプルコードですが、サンプルコードでは、強引にスクロールさせて、さらにプライオリティがBackgroundで指定されているイベントを強引に発砲させてレタリングさせ、取得したいDataGridCellとそのコンテンツ用コントロールを取得しました。
<Window x:Class="Art55.DataGridCell20140305_001.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">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="dataGrid" />
        <Button Grid.Row="1" Click="OnClickick">40行2列目のセルにフォーカスを当てる。</Button>
    </Grid>
</Window>
using System.Data;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Art55.DataGridCell20140305_001
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            DataTable source = CreateSampleData();
            dataGrid.ItemsSource = source.DefaultView;
        }

        private void OnClickick(object sender, RoutedEventArgs e)
        {
            var dataGridCellInfo = new DataGridCellInfo(dataGrid.Items[40], dataGrid.Columns[2]);
            dataGrid.ScrollIntoView(dataGridCellInfo.Item);
            DoEvents();
            FrameworkElement contentElement = dataGridCellInfo.Column.GetCellContent(dataGridCellInfo.Item);
            if (contentElement == null)
            {
                return;
            }
            var dataGridCell = contentElement.Parent as DataGridCell;
            if (dataGridCell == null)
            {
                return;
            }
            dataGridCell.Focus();
        }

        private static void DoEvents()
        {
            // NOTE: http://msdn.microsoft.com/ja-jp/library/system.windows.threading.dispatcher.pushframe(VS.80).aspx
            var frame = new DispatcherFrame();
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
                new DispatcherOperationCallback(ExitFrames), frame);
            Dispatcher.PushFrame(frame);
        }

        private static object ExitFrames(object f)
        {
            ((DispatcherFrame)f).Continue = false;
            return null;
        }

        private static DataTable CreateSampleData()
        {
            var source = new DataTable();
            source.Columns.Add("Column1");
            source.Columns.Add("Column2");
            source.Columns.Add("Column3");
            source.Columns.Add("Column4");

            Enumerable.Range(0, 100)
                .GroupBy(n => Enumerable
                    .Range(0, source.Columns.Count)
                    .Select(m => ((char)('A' + m))
                        .ToString(CultureInfo.InvariantCulture) + n)
                    .Cast<object>()
                    .ToArray()
                    , (n, m) => n)
                .ToList()
                .ForEach(item => source.Rows.Add(item));

            source.AcceptChanges();
            return source;
        }
    }
}

ポイントをまとめると

1.DataGridCell.Focus()を呼び出すことで、セルにフォーカスを当てることができる。
2.DataGridCellは仮想モードの場合、見えていないと存在しない可能性がある。つまり見せる必要がある。

以上

【WPF】手入力不可能なComboBoxに初期値を表示するには?

  • 2014.03.01 Saturday
  • 15:25
JUGEMテーマ:コンピュータ

 WPFのComboBoxには、二つのモードが存在します。一つは、手入力不可のモード。もう一つは、手入力可能なモードです。手入力不可能なモードは、基本的に、ComboBoxをドロップダウンした時に出てくる候補を選択することを目的としたコンロトールですので、候補以外の値を初期値として表示することはできません。逆に、手入力可能な場合は、候補以外の値も入力可能で、初期値もなんでも入ります。という仕様になっているため前回紹介した

【WPF】DataGridに編集可能なComboBoxを表示するには?
http://pro.art55.jp/?eid=1304201


で紹介させていただいたような、手入力可と不可が切り替わるような状況で技術的な問題が発生するということになります。前回は、これをDataGridの問題としてとらえ、解決案を提案させていただきましたが、今回はComboBoxの動きをもう少し正確に見ていきたいと思います。ちなみに個人的にはComboBoxは嫌いなコントロールです。タッチ操作もしづらいこともあり、そのうちなくなってくれたらうれしいな〜なんて思っていたりします。消えろ!

手入力不可能なComboBoxに初期値を表示したい場合として、コードで記述すると以下のコードとなります。

            ComboBox comboBox = ...
            comboBox.ItemsSource = new[]
            {
                new { Text = "あいうえお", Value = 0 },
                new { Text = "かきくけこ", Value = 1 },
                new { Text = "さしすせそ", Value = 2 },
                new { Text = "たちつてと", Value = 3 },
            };

            comboBox.SelectedIndex = 3;

        <ComboBox ...
                  DisplayMemberPath="Text">

上のコードでは「たちつてと」というレコードを選択するために、SelectedIndexプロパティに3を設定しました。
これを実行すれば初期値が


ほかにも方法があり、ちょっとトリッキーな感じがしますが、以下でもいけます。

            ComboBox comboBox = ...
            comboBox.ItemsSource = new[]
            {
                new { Text = "あいうえお", Value = 0 },
                new { Text = "かきくけこ", Value = 1 },
                new { Text = "さしすせそ", Value = 2 },
                new { Text = "たちつてと", Value = 3 },
            };

            comboBox.Text = "たちつてと";

        <ComboBox ...
                  DisplayMemberPath="Text">

わざわざItemsSourceに匿名クラスのインスタンスを突っ込んでいるので、これで初期表示が「たちつてと」となるのが、不思議な感じがしますが、実際に実行してみると3行目のオブジェクトが選択状態になります。この辺りは、ComboBoxに存在する依存関係プロパティが相互にうまいこと作用してくれるのだと思うしかありません。

で、よくやらかす間違いがこれです。

            ComboBox comboBox = ...

            comboBox.Text = "たちつてと";
            comboBox.ItemsSource = new[]
            {
                new { Text = "あいうえお", Value = 0 },
                new { Text = "かきくけこ", Value = 1 },
                new { Text = "さしすせそ", Value = 2 },
                new { Text = "たちつてと", Value = 3 },
            };

        <ComboBox ...
                  DisplayMemberPath="Text">



C#コードで書くと、基本上から順に実行されるため、先にTextプロパティに値を設定させてしまいます。そうすると、候補に存在する値のみ表示可能という制限に引っ掛かり値が設定されません。デバッグで追ってみるとわかりますが、Textプロパティには、設定した値が、設定されています。ただし、SelectedItem等の選択状態を表すプロパティは未選択な状態を返してきます。

とまあ、強引にTextプロパティからでも選択状態を変更できるということを紹介したかったのですが、デメリットもあるということです。

まとめると

1.手入力不可能なComboBoxの初期値を表示するには、候補を選択上にする必要がある。
2.選択状態の変更の手段としてSelectedXXXXプロパティ以外にもTextプロパティも利用できる。
3.Textプロパティを利用するメリットとして、可視化された実際に値を直接指定できる。
4.Textプロパティを利用するデメリットとして、ほかのプロパティも手順は意識する必要はあるが、
  選択状態を変更する意図としては、読めず表示する値を変更しているようにしか見えないため、  
  Textプロパティに関しては誤解が生じやすいコードとなる。

まあ、ComboBoxは嫌いですわ。本当。

【WPF】DataGridに編集可能なComboBoxを表示するには?

  • 2014.03.01 Saturday
  • 14:06
JUGEMテーマ:コンピュータ
-----------------------------------------------------------------------------------
本投稿で紹介した内容に問題があったため以下の投稿で修正版を紹介しています。
【WPF】手入力不可能なComboBoxに初期値を表示するには?【修正版】
-----------------------------------------------------------------------------------

 久々にWPFネタを書いてみようと思います。今回は「DataGridに編集可能なComboBoxを表示するには?」というタイトルです。ターゲットは.NET Framework 4.5.1です。大昔にWPFTookit(最新版ではないと思います)で同じことを実現するコードは紹介したような気がしまうが、正式に.NET FrameworkがDataGridをサポートするようになってからは、技術情報を載せた記憶がないので、これが初めてだと思います。そんなことはどうでもいいのですが、重要な要点は、「過去の記事は古いから参考にならないよ」ってところですかね。まるで他人事みたいな言い方でごめんなさい。

 前置きは、これぐらいにしておいて、今回実現することは、以下の要件を満たすことです。

1.DataGridで値を表示する。
2.セルは編集可能であること。
3.編集中のセルはComboBox(ドロップダウンリストが表示されるコントロール)であること。
4.編集中のセルは手入力可能であること。

この要件を満たすにあたり技術的な問題が発生します。いや、無知だと発生するというだけです。何かしらの解決策をご存知の方は発生しません。いや、本当。無知な私は以下の問題が発生しました。

いろいろ試行錯誤したんですが、「いろいろ」なのでとりあえず、シンプルに書いたコードだけ紹介します。

<Window x:Class="ComboBoxDataGrid20140301_001.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:y="clr-namespace:ComboBoxDataGrid20140301_001"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <!-- 既定のDataGridのスタイル -->
        <Style TargetType="DataGrid">
            <Setter Property="AutoGenerateColumns" Value="False" />
            <Setter Property="CanUserAddRows" Value="False" />
            <Setter Property="CanUserDeleteRows" Value="False" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="SampleDataGrid">
            <DataGrid.Columns>
                <DataGridComboBoxColumn Header="Column1"
                                        TextBinding="{Binding Column1, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                        />
            </DataGrid.Columns>
        </DataGrid>
       
        <Button Grid.Row="1" Click="OnContentChanged">メモリ上のデータを表示する。</Button>
    </Grid>
</Window>

using System.Data;
using System.Linq;
using System.Text;
using System.Windows;

namespace ComboBoxDataGrid20140301_001
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            var dataTable = new DataTable() { TableName = "SampleData" };
            dataTable.Columns.Add("Column1");
            dataTable.Columns.Add("Column2");
            dataTable.Columns.Add("Column3");

            Enumerable.Range(0, 10)
                .Select(n => new object[] { "A" + n, "B" + n, "C" + n })
                .ToList()
                .ForEach(item => dataTable.Rows.Add(item));

            SampleDataGrid.ItemsSource = dataTable.DefaultView;
        }

        private void OnContentChanged(object sender, RoutedEventArgs e)
        {
            string message = SampleDataGrid
                .ItemsSource
                .OfType<DataRowView>()
                .Select(rowView => string.Join(", ", rowView.Row.ItemArray))
                .Aggregate(new StringBuilder(), (sb, line) => sb.AppendLine(line))
                .ToString();
            MessageBox.Show(message);
        }
    }
}

上記のコードを実行してみると、要件を全然満たせていないことがわかります。

1.参照モードで値が表示されていない。
2.編集開始時に値が表示さていない。
3.編集中のComboBoxコントロールは手入力できない。

唯一編集中はセルがComboBoxになるという点だけ要件を満たせています。うん。どうしたものかといところです。この問題を解決するには、大きく分けて二つ手があると思います。DataGridComoBoxColumnのEditingElementStyleおよびElementStyleからStyleを変更し、要件を満たす。もう一つはDataGridComboBoxColumnの利用をやめて、DataGridTemplateColumnを利用する。今回はStyleを編集する方向で、実現することにしました。

変更したソースは以下です。

<Window x:Class="ComboBoxDataGrid20140301_001.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:y="clr-namespace:ComboBoxDataGrid20140301_001"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>

        <y:TextBlockComboBoxItemsSourceConverter x:Key="TextBlockComboBoxItemsSourceConverterKey" />

        <!-- 編集モード時のComboBox -->
        <Style TargetType="ComboBox" x:Key="EditingElementStyle">
            <Setter Property="IsEditable" Value="True" />
        </Style>

        <!-- 参照モード時のComboBox -->
        <Style TargetType="ComboBox" x:Key="TextBlockComboBoxStyle">
            <!--<Setter Property="Text" Value="{Binding Column1}" />-->
            <Setter Property="ItemsSource"
                    Value="{Binding .,
                            Converter={StaticResource TextBlockComboBoxItemsSourceConverterKey},
                            ConverterParameter=Column1}" />
        </Style>

        <!-- 既定のDataGridのスタイル -->
        <Style TargetType="DataGrid">
            <Setter Property="AutoGenerateColumns" Value="False" />
            <Setter Property="CanUserAddRows" Value="False" />
            <Setter Property="CanUserDeleteRows" Value="False" />
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <DataGrid x:Name="SampleDataGrid">
            <DataGrid.Columns>
                <DataGridComboBoxColumn Header="Column1"
                                        TextBinding="{Binding Column1, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
                                        EditingElementStyle="{StaticResource EditingElementStyle}"
                                        ElementStyle="{StaticResource TextBlockComboBoxStyle}"

                                        />
            </DataGrid.Columns>
        </DataGrid>

        <Button Grid.Row="1" Click="OnContentChanged">メモリ上のデータを表示する。</Button>
    </Grid>
</Window>

using System;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;

namespace ComboBoxDataGrid20140301_001
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();

            var dataTable = new DataTable { TableName = "SampleData" };
            dataTable.Columns.Add("Column1");
            dataTable.Columns.Add("Column2");
            dataTable.Columns.Add("Column3");

            Enumerable.Range(0, 10)
                .Select(n => new object[] { "A" + n, "B" + n, "C" + n })
                .ToList()
                .ForEach(item => dataTable.Rows.Add(item));

            SampleDataGrid.ItemsSource = dataTable.DefaultView;
        }

        private void OnContentChanged(object sender, RoutedEventArgs e)
        {
            string message = SampleDataGrid
                .ItemsSource
                .OfType<DataRowView>()
                .Select(rowView => string.Join(", ", rowView.Row.ItemArray))
                .Aggregate(new StringBuilder(), (sb, line) => sb.AppendLine(line))
                .ToString();
            MessageBox.Show(message);
        }
    }

    public class TextBlockComboBoxItemsSourceConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string columnName = (parameter as string) ?? string.Empty;
            var dataRowView = value as DataRowView;
            if (dataRowView == null
                || dataRowView.Row == null
                || !dataRowView.Row.Table.Columns.Contains(columnName))
            {
                return DependencyProperty.UnsetValue;
            }
            return new[] { dataRowView.Row[columnName] };
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

}

すべてのを説明するのは面倒なので、要点だけ書くと

1.ComboBoxを手入力可能な設定にした。IsEditable=True
2.参照モードのComboBoxに値を表示できる値は、候補の値に限られているため
  参照モードのみ候補に値を一件仕込んだ。
  <Setter Property="ItemsSource" ... />

 上記のことをすれば、参照モードでは、値が表示され、編集中は手入力可能なComboBoxが表示されるようになります。補足としては、データソースにDataTableを利用しています。それぞれのカラムにプリミティブな値しか設定しませんが、データソース側に多少の努力を加えると、IValueConverterあたりの下りは不要になるかもしれません。この辺は開発・保守の観点から費用対効果で結論を出すべきところですかね。今回は、知ってる知識で最短で組める方法でがんばってみました。


 今回はComboBoxに候補を出すということはしていませんが、これに関しても色々方法があり、満たしたい要件で実装方法、難易度(知ってれば難易度なんてあがりませんが)が上がります。

 以上で、今回は以上で終わります。久々にBlogを書くこと書き方忘れますね。

-----------------------------
(追記 2014/03/05)
Styleに不要なSetterがあったため修正しました。
誤)
<Setter Property="Text" Value="{Binding Column1}" />
正)
<!--<Setter Property="Text" Value="{Binding Column1}" />-->

TextBindingで上書きされる項目なので指定しても無効となります。

【NetOffice】【WPF】WpfアプリケーションからExcelをアタッチして操作する。

  • 2012.12.20 Thursday
  • 22:03
JUGEMテーマ:コンピュータ

--------------------------------------------
(追記)
私の環境では二つ以上起動した場合に、動作がおかしいことが分かりました。
現在、原因を調査中。
--------------------------------------------

--------------------------------------------
Summary
他のアプリケーションから、スタンドアローンで起動しているExcelアプリケーションのブックを操作することができます。

--------------------------------------------
Source and Project
Source and Project(修正版)

Excelアプリケーションを操作したい場合、

・Excelを手動で操作する。
・VBAで操作する。
・COMアドインなどでExcelの機能を拡張して内部から操作する。
・COM経由でExcelアプリケーションにアタッチして外部から操作する。


という具合に何通りかの方法で実現できますが、今回は最後の、Excelにアタッチして操作する方法を紹介します。といっても、すでに何度か紹介している方法なのですが、今回はWPFを使ってというのが、いつもと少しだけ違うところです。

今回はコード量がブログに収まりそうにないので、要点だけ。

using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
using Art55.NetOfficeApp20121220_001.Commons;
using Art55.NetOfficeApp20121220_001.Utils;
using Excel = NetOffice.ExcelApi;
namespace Art55.NetOfficeApp20121220_001.ViewModels
{
    public class ExcelControlViewModel : NotifyPropertyChangedBase
    {
        public ExcelControlViewModel()
        {
            ExcelApplications = new ObservableCollection<Excel.Application>();
            SearchCommand = new CommandObject(this, OnSearch);
            AttachCommand = new CommandObject(this, OnAttach, CanAttach);
        }
        public ICommand SearchCommand { get; private set; }
        private void OnSearch(object arg)
        {
            ExcelApplications
                .ToList()
                .ForEach(app => app.Dispose());
            ExcelApplications.Clear();
            RunningObjectTableUtils
                .GetActiveProxiesFromRot("Excel", "Application")
                .Select(comObject => new Excel.Application(null, comObject))
                .ToList()
                .ForEach(ExcelApplications.Add);
        }
        public ICommand AttachCommand { get; private set; }
        private void OnAttach(object args)
        {
            Excel.Application selectedApp = _selectedApplication;
            if (selectedApp == null)
            {
                return;
            }
            var workbook = selectedApp.Workbooks.FirstOrDefault();
            if (workbook == null)
            {
                return;
            }
            var sheet = (Excel.Worksheet) workbook.ActiveSheet;
            sheet.Range("A1").Interior.Color = Excel.Enums.XlRgbColor.rgbBurlyWood;
        }
        private bool CanAttach(object arg)
        {
            Excel.Application selectedApp = _selectedApplication;
            if (selectedApp == null)
            {
                return false;
            }
            var workbook = selectedApp.Workbooks.FirstOrDefault();
            if (workbook == null)
            {
                return false;
            }
            return true;
        }
        public ObservableCollection<Excel.Application> ExcelApplications { get; private set; }
        private Excel.Application _selectedApplication;
        public Excel.Application SelectedApplication
        {
            get { return _selectedApplication; }
            set
            {
                if (_selectedApplication != value)
                {
                    _selectedApplication = value;
                    OnNotify("SelectedApplication");
                }
            }
        }
    }
}

■起動中のExcelアプリケーションを検索する。
private void OnSearch(object arg)メソッドで実現しています。NetOfficeの
NetOffice.ExcelApi.Applicaion.GetInstances()メソッドを利用すれば、起動中のExcelアプリケーションをNetOffice.ExcelApi.Applicaionクラスでラップされた状態で取得できるのですが、バグがあるため私の環境では動作しません。なので、ROT(RunningObjectTable)を検索するコードを自作しています。NetOfficeとやりたいことは変わりません。ROTからExcelのモニカを見つけた、Comプロキシを取得しているだけです。ただし、どうもExcel ApplicationとなのつくGUIDが二つ登録されているらしく、Excelを起動するとExcel Applicationが二つ見つかってしまいます。今回は二つ見つかるのは、そのまま放置しています。

■アタッチしたExcelアプリケーションを操作する。
private void OnAttach(object args)
メソッドで実現しています。NetOffice.ExcelApi.Applicationの操作は、過去に何度も紹介しているので割愛します。

今回、ExcelControlViewModel で作成したコマンドやコレクションは以下の通りです。
・ExcelApplications 起動中のExcelアプリケーションのコレクション
・SearchCommand 起動中のExcelアプリケーションを検索するコマンド
・AttachCommand 指定したExcelアプリケーションを操作するコマンド
・SelectedApplication 選択状態にあるExcelアプリケーション

こららをpublicで公開しXAML側にバインドさせます。

<UserControl x:Class="Art55.NetOfficeApp20121220_001.Views.ExcelControlView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid Grid.Row="1" Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid Grid.Row="0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <Button Grid.Column="1" Width="80" Command="{Binding SearchCommand}">検索</Button>
            </Grid>
            <ListBox Grid.Row="1" Width="300" Height="190"
                     ItemsSource="{Binding ExcelApplications}"
                     SelectedItem="{Binding SelectedApplication}"></ListBox>
            <Button Grid.Row="2" Command="{Binding AttachCommand}">アタッチしてActiveSheetのセルA1に悪戯</Button>
        </Grid>
    </Grid>
</UserControl>

実行すると



Excelアプリケーションを一つ起動させた状態で、検索すると二つ行が表示されます。一つを選択し、「アタッチしてActiveSheetのセルA1に悪戯」ボタンを押すと



と、言う具合に、A1に色がつきました。

今回は色を付けるだけでしたが、当然もっと複雑な事もできます。シートのデータを参照したり、書き換えたり、保存したり、色々できます。

Source and Project

--------------------------
(雑記)
昨日、Blogの投稿中に2度もIEが落ちてすごくやる気がなくなりました。ということで一日おいて投稿しました。

--------------------------
(追記)
このアプリケーションは、Excelアプリケーションを検索し、見つけ次第アタッチするため、アプリケーションの終了時にNetOffice.ExcelApi.AppcalitionインスタンスをDisposeする必要がりますが、ソースコード中に処理がぬけています。

アタッチした状態で、WPFアプリケーションより先にExcelアプリケーションを落とすとゾンビが残るので注意が必要です。

Source and Project(修正版)

calendar

S M T W T F S
     12
3456789
10111213141516
17181920212223
24252627282930
31      
<< March 2024 >>

あわせて読みたい

あわせて読みたいブログパーツ

selected entries

categories

archives

recent comment

  • 【WPF】DataGridに編集可能なComboBoxを表示するには?
    art55 (07/16)
  • 【WPF】DataGridに編集可能なComboBoxを表示するには?
    arisa (07/16)
  • 【キーボード】6年前のRealForceを復活させることはできる!?その3
    art55 (05/22)
  • 【キーボード】6年前のRealForceを復活させることはできる!?その3
    分解大好き (05/18)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    art55 (02/04)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    Gen (02/04)
  • 【キーボード】RealForce が壊れて帰ってきた。
    art55 (04/29)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    art55 (02/23)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    かるあ (02/22)
  • 【C#】Dictionaryの実装・データ構造・アルゴリズムを観察する。
    art55 (01/16)

recent trackback

recommend

recommend

recommend

C#プログラマのための.NETアプリケーション最適化技法 (Programmer's SELECTION)
C#プログラマのための.NETアプリケーション最適化技法 (Programmer's SELECTION) (JUGEMレビュー »)
Sasha Goldshtein,Dima Zurbalev,Ido Flatow,サシャ・ゴルドシュタイン,ディマ・ズルバレフ,イド・フラトー

recommend

ろんりと集合
ろんりと集合 (JUGEMレビュー »)
中内 伸光
とてもわかりやすいです。

recommend

recommend

シャノン・ノイマン・ディジタル世界
シャノン・ノイマン・ディジタル世界 (JUGEMレビュー »)
市川 忠男
4章がリレーショナルデータベースな内容になってます。ページ数があまりありませんが、ポイントがものすごく的確にまとまっていて、感動します。

recommend

recommend

東プレ Realforce91UBK-S 静音キーボード 静電容量無接点方式 変荷重 ブラック NG01BS
東プレ Realforce91UBK-S 静音キーボード 静電容量無接点方式 変荷重 ブラック NG01BS (JUGEMレビュー »)

テンキーレス、静音のRealForce91UBK-S。スコスコ感がたまらなく気持ちいいです。家と会社で2台持ってます。

recommend

recommend

プログラミング.NET Framework 第4版 (プログラミングシリーズ)
プログラミング.NET Framework 第4版 (プログラミングシリーズ) (JUGEMレビュー »)
Jeffrey Richter
発売予定美 2013年10月10日。.NET Frameworkとお付き合いする人のバイブルですね。

recommend

recommend

キャット・シッターの君に。
キャット・シッターの君に。 (JUGEMレビュー »)
喜多嶋 隆
私のイラストレータデビュー本です。

recommend

Essential .NET ― 共通言語ランタイムの本質
Essential .NET ― 共通言語ランタイムの本質 (JUGEMレビュー »)
ドン・ボックス,クリス・セルズ,Don Box,Chris Sells,吉松 史彰

links

profile

search this site.

others

mobile

qrcode

powered

無料ブログ作成サービス JUGEM