LoginSignup
1
1

Microsoftの公式ドキュメントから調べる、コレクションを反映したViewの実現

Last updated at Posted at 2024-02-24

問題意識

ViewModelからViewへの値の反映、本記事では特に、ViewModelにあるコレクションをViewに反映させることを考えます。例えば、以下のような挙動を実現させることを問題とします。

  • ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増える

また、タイトルにもある通り、原則、Microsoftの公式から手に入る情報の解釈で、挙動の実現を探っています。本記事では、以下の方針で調査・実装を行います。

  • クラス名やメソッド名など、重要なキーワードについてはどこから調べてもよい
  • ただし、実装に関しては公式ドキュメントを根拠にした解釈によって行う

というのは、公式ドキュメントから情報を得て、実装できるようになることが私自身の目標としてあるからです。

問題のView

<Window x:Class="WPF_MVVM_20240104.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WPF_MVVM_20240104"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="選択ファイル数"/>
            <ComboBox SelectedValuePath="Content" SelectedValue="{Binding Selected}">
                <ComboBoxItem Content="1"/>
                <ComboBoxItem Content="2"/>
                <ComboBoxItem Content="3"/>
                <ComboBoxItem Content="4"/>
                <ComboBoxItem Content="5"/>
                <ComboBoxItem Content="6"/>
                <ComboBoxItem Content="7"/>
                <ComboBoxItem Content="8"/>
                <ComboBoxItem Content="9"/>
                <ComboBoxItem Content="10"/>
            </ComboBox>
        </StackPanel>
        <StackPanel>
            <TextBlock Text="ファイルを選択"/>
            <StackPanel Orientation="Horizontal">
                <TextBox Text="{Binding FilePath}" IsReadOnly="True" MinWidth="100"/>
                <Button Content="選択"/>
            </StackPanel>
        </StackPanel>
    </StackPanel>
</Window>

問題のViewModel

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace WPF_MVVM_20240104
{
    internal class MainWindowViewModel : INotifyPropertyChanged
    {
        private int _selected = 0;

        private string _filePath = "";

        public event PropertyChangedEventHandler PropertyChanged;

        public int Selected
        {
            get { return _selected; }
            set { _selected = value; }
        }

        public string FilePath
        {
            get { return _filePath; }
            set 
            {
                _filePath = value;
                OnPropertyChanged();
            }
        }

        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

現状の外観

ViewのデータコンテキストにViewModelを割り当て実行すると、画像のように、ダイアログで選択したもののファイル名がテキストボックスに反映されていることが分かります。
また、コンボボックスでは1~10の値を選択できます(本記事では特に変更は加えません)。

image.png

それでは

  • ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増える

こちらの解決に向けて調査を進めていきます。

調査

公式以外

実をいうと、公式以外の情報源からで、実装の部分までほとんど参考になってしまいます。

まず、公式のWPFドキュメントに、WPFに関するトピックが体系的にまとまっています。
ここに、「データバインディング」に関する項目があり「どうやら、コレクションをバインドすることが重要になりそうだ」ということが分かります。

そこで、外部サイトを調べることにします。ItemTemplateDataTemplateなどのキーワードも得られ、実装までわかりやすい説明を得ることができます。

しかし、「公式ドキュメントから調べる」と題している以上、これを根拠とはせずに、同様の実装をできるように調査をさらに進めます。

公式

先ほど挙げた公式ドキュメントから、コレクションをバインドすることに関するヒントを集めてみます。まずは、以下の文章に注目できると思います。

コレクションの挿入または削除によって UI が自動的に更新されるように動的バインドを設定するには、コレクションはINotifyCollectionChangedインターフェイスを実装する必要があります。

このことから、

  • ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増える

という問題の解決としてINotifyCollectionChangedインターフェースの実装が考えられます。

WPF には、INotifyCollectionChangedインターフェイスを公開するデータ コレクションの組み込みの実装であるObservableCollection<T>クラスが用意されています。

独自のコレクションを実装する前に、ObservableCollection<T>または既存のコレクション クラス (List<T>Collection<T>BindingList<T>など) のいずれかを使用することを検討してください。

また、これらを見るに、INotifyCollectionChangedインターフェースを実装する手段として、

  • ObservableCollection<T>クラスの使用

をすることが推奨されていることがわかります。
しかしながら、どのようにバインドを行うのか、という点で疑問が残ります。

コレクションをどのようにバインドするか

読み方が汚くて申し訳ないですが、少し前に戻ります。すると以下の記述から、バインドをどのように行うかが見えてきます。

この図に示すように、ItemsControlをコレクション オブジェクトにバインドするにはItemsControl.ItemsSourceプロパティを使用します。

コレクションのクラスのオブジェクトにバインドするときには、どうやら以下のことをする必要があるようです。

  • ItemsControlをコレクションにバインドする
  • そのとき、ItemsControl.ItemsSourceプロパティを使用してバインドする

では、ItemsControlとは一体なんでしょうか。さらにページを少し遡ります。

一般的なシナリオでは、ListBoxListViewTreeViewなどのItemsControlを使用して、データ コレクションを表示します。

ItemsControlの一例として、ListBoxListViewTreeViewなどが用意されていることが伺えます。
以下のページから定義もみてみましょう。

アイテムのコレクションの表現に使用できるコントロールを表します。

少し拍子抜けしてしまうような定義ですね。しかし、コレクションをどのようにバインドするかという問いについては、以下のように説明を考えることができます。

  • コレクションをバインドするには、コレクションを表せるコントロール(=ItemsControlクラス)のプロパティにバインドする
  • たとえば、ListBoxListViewTreeViewのプロパティにバインドできる

コレクションの表現に使用できる、TextBoxの集合

上の箇条書きを踏まえて、改めて当初の課題を振り返ってみます。

  • ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増える

実のところ、先ほどの調査結果だけでは、この課題を解決することはできません。というのも、ItemsControlというコントロールは、TextBoxのコレクションそのものではないからです。ListBoxListViewTreeViewについても、もちろんTextBoxのコレクションではありません。

ここで、「コレクションの表現に使用できる、TextBoxの集合」としてのコントロールを新たに調査し、検討する必要があります。そこで、最初の公式ドキュメントに戻って、TextBoxの集合にコレクションを割り当てる方法がないか、再調査を進めます。

これら 2 つの DataTemplates を使用して、結果として得られた UI が、「データ バインディングとは」セクションに示されているものです。 スクリーン ショットからわかるように、DataTemplates を使用すると、データをコントロールに配置できるだけでなく、データの説得力のあるビジュアルを定義できます。

ドキュメントを読み進めると、このような注目すべき記述がありました。「DataTemplates を使用すると、データをコントロールに配置できる」とあります。また、「スクリーン ショットからわかるように」とあります。どのようなスクリーンショットでしょうか。

データ バインディングの例については、データ バインディング デモに関するページの次のアプリ UI を参照してください。ここでは、オークション品目の一覧が表示されています。
image.png

なるほど、TextBoxは見当たらないものの、☆マークやオレンジ色の枠がコレクションの要素のように表示されています。このあたりのコントロール配置を応用すれば、TextBoxの集合も実装可能であると考えられます。では、DataTemplatesをどのように実装するか。

データ テンプレートの詳細については、「データ テンプレートの概要 (.NET Framework)」を参照してください。

ということで、ここから、TextBoxの集合がどのように実装されるか調査を進めていきましょう。

データテンプレートを使用するにはどうするか

新たに調査を開始したドキュメントでは、データテンプレートがない場合とある場合を比較し、データテンプレートがどういうものかを説明しています。

DataTemplate がないので、現在の ListBox は次のようになります。
image.png

解決策は、DataTemplate を定義することです。 そのための 1 つの方法として、ListBoxItemTemplateプロパティを DataTemplate に設定します。 DataTemplate で指定したものが、データ オブジェクトの視覚的な構造になります。

ここからわかるのは、ListBoxを使うときには、

  • ItemTemplateプロパティをDataTemplateに設定すること
  • DataTemplateに指定をすること

以上のことを守れば、オブジェクトの視覚的な構造を設定できるということです。

とすれば、DataTemplateにはどのように指定を行えばいいのでしょうか。続きを読みます。

次の DataTemplate はかなり単純です。 各項目をStackPanel内の 3 つのTextBlock要素として表示するように指示しています。

<ListBox Width="400" Margin="10"
         ItemsSource="{Binding Source={StaticResource myTodoList}}">
   <ListBox.ItemTemplate>
     <DataTemplate>
       <StackPanel>
         <TextBlock Text="{Binding Path=TaskName}" />
         <TextBlock Text="{Binding Path=Description}"/>
         <TextBlock Text="{Binding Path=Priority}"/>
       </StackPanel>
     </DataTemplate>
   </ListBox.ItemTemplate>
 </ListBox>

DataTemplateの直下に、指定したい視覚的構造をそのまま記述すればよさそうです。

今度は、ListBoxは次のようになります。
image.png

確かに、TextBlock3つのStackPanelの集合として、ListBoxの各要素が定義されています。

では、改めてデータテンプレートの実装方法についてまとめてみます。

  • ListBoxItemTemplateプロパティをDataTemplateに設定すること
  • DataTemplateに任意のコントロールをXAMLで記載すること

いよいよ、TextBoxの各要素にコレクションを割り当てることは不可能ではないと、説明できそうです。

しかし実装に入る前に、あと1点調査をしておくべき部分が考えられます。それは、ListBox以外にはDataTemplateを設定できないのか、という点です。

ListBox以外のDataTemplate実装方法

もともと、DataTemplateを実装するには、ItemTemplateプロパティに指定をすればよいのでした。では、ListBox以外のコントロールに、ItemTemplateプロパティはあるのでしょうか。もし存在すれば、同様に実装できそうです。

まずは、ListViewクラス。

ItemTemplateプロパティが確認されました。

ItemTemplate
各項目を表示するために使用される DataTemplate を取得または設定します。
(継承元 ItemsControl)

ItemsControlの子要素はすべてItemTemplateを持っているというのは重要な情報です。ItemsControlクラスを継承しているすべてのクラスで、ItemTemplateプロパティによる外観の規定が設定可能ではないだろうか。このような考え思い浮かびます。

もう一度、ItemsControlクラスを調査しましょう。ここに例示されているソースコードから、興味深いことがわかります。ItemTemplateプロパティが使われている部分を抜き出して確認しましょう。

機械翻訳が自然な翻訳をしていませんでしたので、以下の記述は、DeepLを使って英語版から日本語訳したものになります。

次の例は、ItemsControlが提供するさまざまなスタイリングおよびテンプレート関連のプロパティの使用方法を示しています。

  <!--Use the ItemTemplate to set a DataTemplate to define
      the visualization of the data objects. This DataTemplate
      specifies that each data object appears with the Priority
      and TaskName on top of a silver ellipse.-->
  <ItemsControl.ItemTemplate>
    <DataTemplate>
      <DataTemplate.Resources>
        <Style TargetType="TextBlock">
          <Setter Property="FontSize" Value="18"/>
          <Setter Property="HorizontalAlignment" Value="Center"/>
        </Style>
      </DataTemplate.Resources>
      <Grid>
        <Ellipse Fill="Silver"/>
        <StackPanel>
          <TextBlock Margin="3,3,3,0"
                     Text="{Binding Path=Priority}"/>
          <TextBlock Margin="3,0,3,7"
                     Text="{Binding Path=TaskName}"/>
        </StackPanel>
      </Grid>
    </DataTemplate>
  </ItemsControl.ItemTemplate>

コメント文も翻訳してみました。

ItemTemplate を使用して、データ・オブジェクトの視覚化を定義する DataTemplate を設定します。このDataTemplateは、各データオブジェクトが銀色の楕円の上にPriorityとTaskNameとともに表示されることを指定します。

この記述から、ListBoxListViewのプロパティというより、親クラスであるItemsControlのプロパティの機能として、データテンプレートによる外観の指定があったのだとわかります。つまり、ItemsControlを継承しているクラスであれば、先ほど調査した方法で問題なく実装ができるということです。

また、以下の記述も見ておきましょう。

<ItemsControl Margin="10"
              ItemsSource="{Binding Source={StaticResource myTodoList}}">
  <!--The ItemsControl has no default visual appearance.
      Use the Template property to specify a ControlTemplate to define
      the appearance of an ItemsControl. The ItemsPresenter uses the specified
      ItemsPanelTemplate (see below) to layout the items. If an
      ItemsPanelTemplate is not specified, the default is used. (For ItemsControl,
      the default is an ItemsPanelTemplate that specifies a StackPanel.-->

ItemsControlには、デフォルトの外観はありません。Template プロパティを使用して ControlTemplate を指定し、ItemsControl の外観を定義します。ItemsPresenterは、指定されたItemsPanelTemplate(後述)を使用してアイテムをレイアウトします。ItemsPanelTemplateが指定されていない場合は、デフォルトが使用さ れます。(ItemsControlの場合、デフォルトはStackPanelを指定するItemsPanelTemplateです。

ItemsControlにデフォルトの外観がないというのも重要な情報です。今回のケースのように、ListBoxListViewではなく単に「TextBoxの集合」を目標としている場合には、ゼロから外観を組み立てるItemsControlの方がよいと考えることもできるからです。

調査のまとめ

これで、実装に必要な調査が概ね完了したと言えます。ここまでの調査で、以下のことがわかりました。(上の記述から一部、日本語の表現を変えています。)

  • バインドしたいコレクションは、ObservableCollection<T>クラスとして実装する
  • コレクションをバインドするには、コレクションを表せるコントロール(=ItemsControlクラス)のプロパティにバインドする
  • ItemTemplateプロパティにはDataTemplateを設定すること
  • DataTemplateに要素のコントロールをXAMLで記載すること

これらの調査結果から、当初設定した課題を解決していきます。

実装

ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増える

これが当初設定した課題です。また、既に以下のような外観をもっています。

image.png

では、現状の実装を踏まえて、順番に実装を追加していきます。

バインドしたいコレクションは、ObservableCollection<T>クラスとして実装する

ViewModelはファイルパスの情報をもっておきたいので、ObservableCollection<string>クラスを実装します。

public ObservableCollection<string> Paths { get; set; } = new ObservableCollection<string>();

ItemsControlクラスのプロパティにバインドする

現在、Viewのファイル選択部分は以下のようになっています。

        <StackPanel>
            <TextBlock Text="ファイルを選択"/>
            <StackPanel Orientation="Horizontal">
                <TextBox Text="{Binding FilePath}" IsReadOnly="True" MinWidth="100"/>
                <Button Content="選択" Command="{Binding SelectFile}"/>
            </StackPanel>
        </StackPanel>

ここに、ItemsControlを実装して、そのプロパティに先ほどのObservableCollection<string>をバインドします。

        <ItemsControl ItemsSource="{Binding Paths}"/>

ItemsControlItemTemplate にDataTemplateを設定する

ItemControlにさらに実装を加えます。

        <ItemsControl ItemsSource="{Binding Paths}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

DataTemplateに要素のコントロールをXAMLで記載する

このDataTemplateに最初のXAMLで記述していた、コントロールを追加します。
これによって、ObservableCollectionの要素と、テンプレートがひとつずつ対応するようになります。

        <ItemsControl ItemsSource="{Binding Paths}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="ファイルを選択"/>
                        <StackPanel Orientation="Horizontal">
                            <TextBox Text="{Binding .}" IsReadOnly="True" MinWidth="100"/>
                            <Button Content="選択"/>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

検討

現段階で実行すると、以下のような表示になります

image.png

この実装で、課題が解決したか検討してみます。このままでは、「コレクションの要素が増えると、View上のTextBoxの数も増える」という部分が解決されていない状況です。

そこで、「ComboBox」で選択された値に応じて、要素の数を変更する処理を加えてみます。
ViewModelに変更を加えます。

        public int Selected
        {
            get { return _selected; }
            set
            {
                _selected = value;

                Paths.Clear();
                for (int i = 0; i < value; i++)
                {
                    Paths.Add("not selected");
                }
            }
        }

こうすることで、選択した値に応じて、TextBoxの数も変わることが確認できます。

image.png

以上で、ViewModel内のコレクションの各要素がもつ文字列をViewの各TextBoxに反映させ、コレクションの要素が増えると、View上のTextBoxの数も増えることが確認できました。

実装後のView

<Window x:Class="WPF_MVVM_20240104.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="選択ファイル数"/>
            <ComboBox SelectedValuePath="Content" SelectedValue="{Binding Selected}">
                <ComboBoxItem Content="1"/>
                <ComboBoxItem Content="2"/>
                <ComboBoxItem Content="3"/>
                <ComboBoxItem Content="4"/>
                <ComboBoxItem Content="5"/>
                <ComboBoxItem Content="6"/>
                <ComboBoxItem Content="7"/>
                <ComboBoxItem Content="8"/>
                <ComboBoxItem Content="9"/>
                <ComboBoxItem Content="10"/>
            </ComboBox>
        </StackPanel>
        <ItemsControl ItemsSource="{Binding Paths}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="ファイルを選択"/>
                        <StackPanel Orientation="Horizontal">
                            <TextBox Text="{Binding .}" IsReadOnly="True" MinWidth="100"/>
                            <Button Content="選択"/>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </StackPanel>
</Window>

実装後のViewModel

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;


namespace WPF_MVVM_20240104
{
    internal class MainWindowViewModel : INotifyPropertyChanged
    {
        private int _selected = 0;

        private string _filePath = "";

        public event PropertyChangedEventHandler PropertyChanged;
        
        public ObservableCollection<string> Paths { get; set; } = new ObservableCollection<string>();

        public int Selected
        {
            get { return _selected; }
            set
            {
                _selected = value;

                Paths.Clear();
                for (int i = 0; i < value; i++)
                {
                    Paths.Add("not selected");
                }
            }
        }

        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1