コンテンツへスキップ
XamDataGrid–選択したエディターを使用して列を動的に作成およびデータバインドします

XamDataGrid–選択したエディターを使用して列を動的に作成およびデータバインドします

最近、私の親しい友人から、XamDataGridを使用してオブジェクトのコレクションに基づいて列を動的に生成する方法を尋ねるメールが送られてきました。 これは、ほぼすべてのアプリケーションがおそらく実行する必要がある非常に一般的なタスクのようです。 これは決して珍しいことではありません。

10分読み取り

最近、私の親しい友人から、XamDataGridを使用してオブジェクトのコレクションに基づいて列を動的に生成する方法を尋ねるメールが送られてきました。 これは、ほぼすべてのアプリケーションがおそらく実行する必要がある非常に一般的なタスクのようです。 これは決して珍しいことではありません。

この特定のケースでは、グリッド内の列として表されるオブジェクトのコレクションをフラット化する必要があります。 例えば;実行時までわからないnレベルのプロパティまたは属性を持つオブジェクトがある場合がありますが、グリッド内の 1 行でオブジェクトを編集したい場合があります。オブジェクトに Prop1、Prop2、Prop3 などの多数のプロパティを追加することは望ましくありません。これは、グリッドにバインドできるようにするためです。 何人いるかはわかりません。グリッドに列を動的に追加し、実行時にそれらの列を子コレクション内の正しいオブジェクトにバインドする必要があります。

このシナリオでは、スタッフィングアプリケーションを構築しており、子プロパティとして "Period"オブジェクトのコレクションを持つ "StaffMember"オブジェクトがあります。私のオブジェクトは次のようになります。

public class StaffMember
{
    public String Department { get; set; }
    public String Name { get; set; }
    public IList<Period> Periods { get; set; }

    public StaffMember()
    {
        this.Periods = new List<Period>();
    }
}

public class Period
{
    public string Title { get; set; }
    public int Hours { get; set; }
}

これらは、現在 INotifyPropertyChanged を実装していない単純な POCO にすぎません。 このデモアプリでは、プロパティ通知は必要ありません。 運用アプリケーションでは、ほとんどの場合、変更通知用に INotifyPropertyChanged インターフェイスを実装する必要があります。 次に、ViewModelが必要です。

public class StaffMemberViewModel : INotifyPropertyChanged
{
    ObservableCollection<StaffMember> _staffMembers;
    public ObservableCollection<StaffMember> StaffMembers
    {
        get { return _staffMembers; }
        set
        {
            _staffMembers = value;
            RaisePropertyChanged("StaffMembers");
        }
    }

    public StaffMemberViewModel()
    {
        PopulateStaffMembers();
    }

    void PopulateStaffMembers()
    {
        var list = new ObservableCollection<StaffMember>();
        var rand = new Random();

        for (Int32 i = 1; i < 4; i++)
        {
            var member = new StaffMember { Name = String.Format("Name {0}", i), Department = String.Format("Department {0}", i) };
            for (int j = 1; j < 5; j++)
                member.Periods.Add(new Period { Title = String.Format("Period {0}", j), Hours = rand.Next(0, 160) });
            list.Add(member);
        }

        StaffMembers = list;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(String propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

この ViewModel は、すべての ViewModel と同様に INotifyPropertyChanged インターフェースを実装します。 ご覧のとおり、StaffMembers のコレクションを公開する 1 つのプロパティがあります。 また、ダミーデータを生成する方法もあります。 次に必要なのはビューです。

<Window x:Class="XamDataGridDynamicColumns.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:igWPF="http://schemas.infragistics.com/xaml/wpf"
        xmlns:local="clr-namespace:XamDataGridDynamicColumns"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:StaffMemberViewModel />
    </Window.DataContext>

    <Grid>
        <igWPF:XamDataGrid DataSource="{Binding Path=StaffMembers}"
                           FieldLayoutInitialized="xamDataGrid_FieldLayoutInitialized">
            <igWPF:XamDataGrid.FieldLayoutSettings>
                <igWPF:FieldLayoutSettings AutoGenerateFields="False"/>
            </igWPF:XamDataGrid.FieldLayoutSettings>
            <igWPF:XamDataGrid.FieldLayouts>
                <igWPF:FieldLayout>
                    <igWPF:Field Name="Name"/>
                    <igWPF:Field Name="Department"/>
                </igWPF:FieldLayout>
            </igWPF:XamDataGrid.FieldLayouts>
        </igWPF:XamDataGrid>
    </Grid>
</Window>

まず、ビューで 2 つの名前空間を定義したことに注目してください。 1 つはローカル オブジェクト用で、もう 1 つは使用する Infragistics コントロール用です。 ビューの DataContext を StaffMemberViewModel のインスタンスに設定します。 これは、好きなように行うことができます。 私はたまたまXAMLでそれを行いました。 次に、XamDataGrid を宣言し、それを ViewModel の StaffMembers のコレクションにデータ バインドする必要があります。 ここでの考え方は、StaffMember に常に使用可能な名前と部門があることがわかっているということです。 これらは動的な列ではないため、FieldLayoutで宣言してビューを作成できます。 AutoGenerateField = falseを設定することを確認したいのですが、これは、どの列を作成するかを担当するからです。

では、列の生成を開始するにはどうすればよいでしょうか。 まず、XamDataGrid.FieldLayoutInitialized イベントにイベント ハンドラを追加します。 ここで、作成、データバインド、エディターの選択、列の追加など、すべての魔法が行われます。 ViewModelに小さなプロパティを追加して、必要なデータにアクセスできるようにしました。

public StaffMemberViewModel ViewModel
{
    get { return this.DataContext as StaffMemberViewModel; }
}

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{

}

ViewModel プロパティは、ビューの DataContext を StaffMemberViewModel の形式で提供するだけです (これを使用してすぐにごまかします)。 StaffMember の Periods コレクション内の Period の数に基づいて列を作成する必要があります。 ランタイムまでPeriodがいくつあるかわからないため、最初にこの情報を取得する必要があることを忘れないでください。 ここで、デモの目的でチートします。

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        e.FieldLayout.Fields.Add(field);
    }
}

このデモでは、コレクション内の最初の StaffMember を使用して、作成する列の数を決定します。 もちろん、現実の世界では、子コレクションの最初のインデックスを使用して、構築する列の数を把握することは望ましくありません。私は、どの列といくつの列を作成するかを定義するある種の定義オブジェクトをお勧めします。 次に、正しい数のインターレーションでループを作成し、新しい UnboundField を作成して作成します。 3つの重要なプロパティを設定します。 最初のものは名前です。 これにより、列ヘッダーが表示されます。 次は BindingMode です。 データ バインディングを TwoWay にしたいと考えています。 最後に、BindingPath を作成します。 インデクサー ([ ]) を使用するバインディング パスを作成していることに注意してください。 これにより、フラット化するコレクションの特定のインデックスにあるオブジェクトのバインディングを作成できます。 最後に、新しく作成したフィールドをFieldLayoutに追加するだけです。 アプリケーションを実行すると、これが得られるものです。

アプリケーションを実行すると、これが得られるものです。

かなりいいですね。 オブジェクトグラフを正常にフラット化し、各セルに対して基になるオブジェクトの各プロパティに適切なデータバインディングを作成しました。

Choosing Your Editor

今、私はあなたが何を考えているか知っています。 しかし、ブライアン、デフォルトのエディターはTextBlockです。 XamNumericEditorのような別のエディタを使用したい場合はどうすればよいですか。 まあ、あなたにとって幸運なことに、それはとても簡単です。 必要なのは、いくつかのコードと少しの XAML を追加するだけです。

private void xamDataGrid_FieldLayoutInitialized(object sender, Infragistics.Windows.DataPresenter.Events.FieldLayoutInitializedEventArgs e)
{
    //a cheat to get the number of columns to create.
    var staffMember = this.ViewModel.StaffMembers.First();

    for (Int32 i = 0; i < staffMember.Periods.Count; i++)
    {
        var field = new UnboundField
        {
            Name = staffMember.Periods[i].Title,
            BindingMode = BindingMode.TwoWay,
            BindingPath = new PropertyPath(String.Format("Periods[{0}].Hours", i))
        };

        field.Settings.EditAsType = typeof(Int32);
        field.Settings.EditorStyle = (Style)Resources["HoursFieldStyle"];

        e.FieldLayout.Fields.Add(field);
    }
}

イベントハンドラに 2 行のコードを追加しました。 Field.Settings.EditAsType プロパティを設定することで、使用しているエディターにデータ型の処理方法を指示しています。 Hours プロパティは Int 型であるため、それに応じてプロパティを設定します。 さて、これでは XamNumericEditor は自動的には出ません。 そのためには、EditorStyle を提供する必要があります。 そこで、Field.Settings.EditorStyle プロパティに、これから作成するリソースから値を取得するように指示するコード行を追加します。

<Window.Resources>
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>
</Window.Resources>

XAML を開き、ビューの Window.Resources プロパティ内でスタイルを定義します。 ここで行っているのは、XamNumericEditor を使用することを指定し、そのエディターの Mask プロパティを 3 桁に設定することです。 さて、ランダムデータは3桁未満ですが、これは例として使用されているに過ぎないことに注意してください。 アプリケーションを実行して、何が得られるか見てみましょう。

アプリケーションを実行して、何が得られるか見てみましょう。

現在は、マスクが ### の XamNumericEditor を使用しています。定義した ### マスクと一致しない値を入力しようとするとどうなりますか?

定義した ### マスクと一致しない値を入力しようとするとどうなりますか?

そうです! 私たちのエディターは、新しい値が有効な入力に必要なマスクと一致しないことを警告するはずです。 かっこいいでしょ!? そして、それはとても簡単です。

では、コンボボックスはいかがでしょうか。

さて、ブライアン、あなたは私たちに簡単なものを見せてくれましたが、もっと複雑なものはどうですか? 私はXamComboEditorを使用し、各セルのフォームを選択するための値を入力させたいと思います。 私に挑戦しようとしているの? 持って来る。それ。

さて、まず最初に。 XamComboEditor のデータソースが必要です。 これにはいくつかのアプローチがありますので、1つだけ選びます。 私は、その値を表すオブジェクトと、データソースとして使用するクラスが必要です。 これは、別のビューモデルである場合もあれば、現在ある同じビューモデル内のイベントである場合もあります。 別途使います。 以下は、XamComboEditor のデータソースを表すクラスです。

public class HoursDataSource
{
    public ObservableCollection<DataItem> Hours { get; set; }

    public HoursDataSource()
    {
        PopulateHours();
    }

    private void PopulateHours()
    {
        var list = new ObservableCollection<DataItem>();

        for (int i = 1; i < 160; i++)
        {
            list.Add(new DataItem() { Name = i.ToString(), Value = i });
        }

        Hours = list;
    }
}

public class DataItem
{
    public string Name { get; set; }
    public int Value { get; set; }
}

とても簡単です。 それほど多くはありません。 次に、XAML にコードを追加する必要があります。

<Window.Resources>
    <local:HoursDataSource x:Key="hoursDataSource" />
    <igWPF:ComboBoxItemsProvider x:Key="hoursProvider" ItemsSource="{Binding Hours, Source={StaticResource hoursDataSource}}" DisplayMemberPath="Name" ValuePath="Value" />
    <Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamComboEditor}">
        <Setter Property="ItemsProvider" Value="{StaticResource hoursProvider}" />
    </Style>
    <!--<Style x:Key="HoursFieldStyle" TargetType="{x:Type igWPF:XamNumericEditor}">
        <Setter Property="Mask" Value="###" />
    </Style>-->
</Window.Resources>

まず、最初に作成したスタイルをコメントアウトします。 次に、HoursDataSource オブジェクトのインスタンスを追加する必要があります。 次に、ComboBoxItemsProvider を作成します。 ItemsSource を HoursDataSource インスタンスに存在する Hours プロパティにデータ バインドします。 DsiplayMemberPath=Name と ValuePath=Value を設定することを忘れないでください。 次に、以前に使用していたスタイルを、TargetType を XamComboEditor に設定する新しいスタイルに置き換えます。 ItemsProvider プロパティを作成したばかりの ComboBoxItemsProvider リソースに設定する Setter を定義します。 アプリを実行して、何が起こるか見てみましょう。

アプリを実行して、何が起こるか見てみましょう。

もちろん、どの列にどのエディターを使用するか、またはさまざまな列に対して値を編集するタイプを決定するロジックを含めることで、これを常に機能化できます。

ソースコードはお気軽にダウンロードしてください。 ご不明な点がございましたら、http://brianlagunas.comのブログ、Twitter(@BrianLagunas)からご連絡いただくか、以下にコメントを残してください。

デモを予約