WPFで汎用セレクターダイアログを作る(選択専用/編集付き)

コンピュータ

WPF には一覧から項目を選択するためのコントロールは用意されていますが、
「選択専用」や「編集機能付き」といった
実用的なセレクターダイアログは標準では用意されていません。

この記事では、コードビハインドのみで実装した
再利用可能な汎用セレクターダイアログを紹介します。

選択専用セレクター

ソースコード

ファイル名:Helpers\SelectorDialog.cs

// セレクターダイアログ
using System.Windows;
using System.Windows.Controls;

namespace Maywork.WPF.Helpers;
public sealed class SelectorDialog<T> : Window
{
    private readonly ListView _listView;
    private readonly Button _okButton;
    private readonly Button _cancelButton;

    public T? SelectedItem { get; private set; }
    
    // コンストラクタ
    public SelectorDialog(IEnumerable<T> items, string? title = null)
    {
        

        // ダイアログのタイトル・サイズの定義
        Title = title ?? "Select item";
        Width = 520;
        Height = 420;
        MinWidth = 360;
        MinHeight = 260;

        // 起動時の位置を親ウィンドウの中央に配置
        WindowStartupLocation = WindowStartupLocation.CenterOwner;

        ResizeMode = ResizeMode.CanResize;  // リサイズ可
        ShowInTaskbar = false;  // タスクバーに表示しない

        // ===== Root Grid =====
        var root = new Grid
        {
            Margin = new Thickness(12)  // マージン
        };
        // <RowDefinition Height="*" /> ... ウィンドウサイズと連動し可変
        root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        // <RowDefinition Height="Auto" /> ... 内容に合わせてサイズが決定
        root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

        // ===== ListView (Row 0) =====
        _listView = new ListView
        {
            Margin = new Thickness(0, 0, 0, 10),  // マージン
            ItemsSource = items?.ToList() ?? new List<T>(), // 引数itemsをアイテムソースとしてセット
        };
        // リストビューの選択が変更された
        _listView.SelectionChanged += (_, __) =>
        {
            // リストビュー選択された状態でOKボタンを有効化
            _okButton!.IsEnabled = _listView.SelectedItem != null;
        };

        Grid.SetRow(_listView, 0);  // リストビューをGridの0行目に配置
        root.Children.Add(_listView);   // リストビューをダイアログ(Window)に登録

        // ===== Buttons Panel (Row 1) =====
        var buttonPanel = new StackPanel
        {
            Orientation = Orientation.Horizontal,   // 水平積み
            HorizontalAlignment = HorizontalAlignment.Right // 右寄せ
        };

        // OKボタン
        _okButton = new Button
        {
            Content = "OK",
            Width = 90,
            IsEnabled = false,
            IsDefault = true, // Enterキー押下で押された扱いに成る
            Margin = new Thickness(0, 0, 8, 0)
        };
        // OKボタンクリックイベント
        _okButton.Click += (_, __) =>
        {
            SelectedItem = (T?)_listView.SelectedItem;
            DialogResult = true;
            Close();
        };

        // キャンセルボタン
        _cancelButton = new Button
        {
            Content = "Cancel",
            Width = 90,
            IsCancel = true, // Escキー押下で押された扱いに成る
        };
        // キャンセルボタンクリックイベント
        _cancelButton.Click += (_, __) =>
        {
            SelectedItem = default;
            DialogResult = false;
            Close();
        };

        // スタックパネルにボタンを追加
        buttonPanel.Children.Add(_okButton);
        buttonPanel.Children.Add(_cancelButton);

        // スタックパネルをGridの1番に
        Grid.SetRow(buttonPanel, 1);
        // スタックパネルをダイアログ(Window)に追加
        root.Children.Add(buttonPanel);

        // ダイアログ(Window)のContentにroot(Grid)をセット
        Content = root;
    }
}

ファイル名:ListSelectorDialogDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:MainWindow.xaml

<Window x:Class="ListSelectorDialogDemo.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:ListSelectorDialogDemo"
        mc:Ignorable="d"
        FontSize="12"
        Title="ListSelectorDialogDemo" Height="200" Width="400">
    <Grid>
        <WrapPanel Margin="16">
            <Button x:Name="EditButton" Padding="8" >選択</Button>
        </WrapPanel>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;

using Maywork.WPF.Helpers;

namespace ListSelectorDialogDemo;

 


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        EditButton.Click += (_, __) =>
        {
            var items = new[] { "りんご", "バナナ", "マンゴー" };

            var dlg = new SelectorDialog<string>(items, "Selector")
            {
                Owner = this // MainWindow など
            };

            if (dlg.ShowDialog() == true)
            {
                var selected = dlg.SelectedItem;
                MessageBox.Show($"選択: {selected}");
            }
        };
    }
}

実行例

  1. 起動→ボタンを押す
  2. ダイアログが表示→選択→OK
  3. 結果が表示される。

構成

MyWpfApp
├─ App.xaml.cs
├─ MainWindow.xaml.cs
│
├─ Helpers
│     ├─ SelectorDialog.cs
│     │   └─ 汎用・選択専用ダイアログ
│     │
│     └─ (どのアプリにも再利用可能)
│
└─ MainWindow.xaml.cs

編集機能付きセレクター

ソースコード

ファイル名:CollectionEditorDialogDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

ファイル名:Helpers\CollectionEditorDialog.cs

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace Maywork.WPF.Helpers;

// 汎用コレクション編集ダイアログ
public sealed class CollectionEditorDialog<T> : Window
{
    private readonly ListView _listView;
    private readonly ObservableCollection<T> _items;

    public T? SelectedItem { get; private set; }

    // 呼び出し元から注入される処理
    public Func<T?>? AddItem { get; init; }
    public Func<T, T?>? EditItem { get; init; }
    public Action<T>? DeleteItem { get; init; }

    public IReadOnlyList<T> ResultItems => _items.ToList();

    public CollectionEditorDialog(IEnumerable<T> items, string? title = null)
    {
        // ---- Window 設定 ----
        Title = title ?? "Collection Editor";
        Width = 520;
        Height = 420;
        MinWidth = 360;
        MinHeight = 260;

        // 起動時の位置を親ウィンドウの中央に配置
        WindowStartupLocation = WindowStartupLocation.CenterOwner;
        ResizeMode = ResizeMode.CanResize;   // リサイズ可
        ShowInTaskbar = false;               // タスクバーに表示しない

        _items = new ObservableCollection<T>(items ?? Enumerable.Empty<T>());

        // ===== Root Grid =====
        var root = new Grid { Margin = new Thickness(12) };
        // <RowDefinition Height="*" />    ... ウィンドウサイズと連動し可変
        root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        // <RowDefinition Height="Auto" /> ... 内容に合わせてサイズが決定
        root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

        // ===== ListView =====
        _listView = new ListView
        {
            Margin = new Thickness(0, 0, 0, 10),
            ItemsSource = _items
        };
        Grid.SetRow(_listView, 0);
        root.Children.Add(_listView);

        // ===== Buttons =====
        var panel = new StackPanel {
            Orientation = Orientation.Horizontal,
            HorizontalAlignment = HorizontalAlignment.Right
        };

        var addBtn    = new Button {
            Content = "Add",
            Width = 90,
            Margin = new Thickness(0, 0, 8, 0)
        };
        var editBtn   = new Button {
            Content = "Edit",
            Width = 90,
            Margin = new Thickness(0, 0, 8, 0),
            IsEnabled = false
        };
        var deleteBtn = new Button {
            Content = "Delete",
            Width = 90,
            Margin = new Thickness(0, 0, 16, 0),
            IsEnabled = false
        };
        var okBtn     = new Button {
            Content = "OK",
            Width = 90,
            IsDefault = true,
            IsEnabled = false
        };
        var cancelBtn = new Button {
            Content = "Cancel",
            Width = 90,
            IsCancel = true
        };

        _listView.SelectionChanged += (_, __) =>
        {
            bool hasSelection = _listView.SelectedItem != null;
            okBtn.IsEnabled = hasSelection;
            editBtn.IsEnabled = hasSelection;
            deleteBtn.IsEnabled = hasSelection;
        };

        // --- Add ---
        addBtn.Click += (_, __) =>
        {
            if (AddItem == null) return;

            var item = AddItem();
            if (item != null)
            {
                _items.Add(item);
                _listView.SelectedItem = item;
            }
        };

        // --- Edit ---
        editBtn.Click += (_, __) =>
        {
            if (EditItem == null) return;
            if (_listView.SelectedItem is not T item) return;

            var edited = EditItem(item);
            if (edited != null)
            {
                int index = _items.IndexOf(item);
                _items[index] = edited;
                _listView.SelectedItem = edited;
            }
        };

        // --- Delete ---
        deleteBtn.Click += (_, __) =>
        {
            if (DeleteItem == null) return;
            if (_listView.SelectedItem is not T item) return;

            DeleteItem(item);
            _items.Remove(item);
        };

        // --- OK / Cancel ---
        okBtn.Click += (_, __) =>
        {
            SelectedItem = (T?)_listView.SelectedItem;
            DialogResult = true;
            Close();
        };

        cancelBtn.Click += (_, __) =>
        {
            DialogResult = false;
            Close();
        };

        panel.Children.Add(addBtn);
        panel.Children.Add(editBtn);
        panel.Children.Add(deleteBtn);
        panel.Children.Add(okBtn);
        panel.Children.Add(cancelBtn);

        Grid.SetRow(panel, 1);
        root.Children.Add(panel);

        Content = root;
    }
}

ファイル名:Helpers\UserEditorDialog.cs

using System.Windows;
using System.Windows.Controls;

namespace Maywork.WPF.Helpers;

// string を1項目編集するだけのエディタダイアログ
public sealed class UserEditorDialog : Window
{
    private readonly TextBox _textBox;

    public string Result { get; private set; } = "";

    // 追加用
    public UserEditorDialog(string? title=null)
        : this(string.Empty, title)
    {
    }

    // 編集用
    public UserEditorDialog(string value, string? title=null)
    {
        Title = title ?? "Edit item";
        Width = 320;
        Height = 140;
        MinWidth = 260;
        MinHeight = 120;

        WindowStartupLocation = WindowStartupLocation.CenterOwner;
        ResizeMode = ResizeMode.NoResize;
        ShowInTaskbar = false;

        // ===== Root =====
        var root = new Grid { Margin = new Thickness(12) };
        root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
        root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

        // ===== TextBox =====
        _textBox = new TextBox
        {
            Text = value,
            MinWidth = 240,
            Margin = new Thickness(0, 0, 0, 10)
        };
        Grid.SetRow(_textBox, 0);
        root.Children.Add(_textBox);

        // ===== Buttons =====
        var panel = new StackPanel
        {
            Orientation = Orientation.Horizontal,
            HorizontalAlignment = HorizontalAlignment.Right
        };

        var okBtn = new Button
        {
            Content = "OK",
            Width = 80,
            IsDefault = true,
            Margin = new Thickness(0, 0, 8, 0)
        };
        okBtn.Click += (_, __) =>
        {
            Result = _textBox.Text;
            DialogResult = true;
            Close();
        };

        var cancelBtn = new Button
        {
            Content = "Cancel",
            Width = 80,
            IsCancel = true
        };
        cancelBtn.Click += (_, __) =>
        {
            DialogResult = false;
            Close();
        };

        panel.Children.Add(okBtn);
        panel.Children.Add(cancelBtn);

        Grid.SetRow(panel, 1);
        root.Children.Add(panel);

        Content = root;
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="CollectionEditorDialogDemo.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:CollectionEditorDialogDemo"
        mc:Ignorable="d"
        FontSize="12"
        Title="CollectionEditorDialogDemo" Height="200" Width="400">
    <Grid>
        <WrapPanel Margin="16">
            <Button x:Name="EditButton" Padding="8" >選択</Button>
        </WrapPanel>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

using System.Windows;

using Maywork.WPF.Helpers;

namespace CollectionEditorDialogDemo;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        EditButton.Click += (_, __) =>
        {
            var items = new List<string>
            {
                "りんご",
                "ばなな",
                "マンゴー"
            };

            var dlg = new CollectionEditorDialog<string>(items, "String Collection")
            {
                Owner = this,

                AddItem = () =>
                {
                    var editor = new UserEditorDialog("追加");
                    if (editor.ShowDialog() != true)
                        return null;

                    var newItem = editor.Result;

                    // --- DB連携する場合はここ ---
                    // INSERT 処理
                    // SaveChanges() 等
                    // ----------------------------

                    return newItem;
                },

                EditItem = item =>
                {
                    var editor = new UserEditorDialog(item, "変更");
                    if (editor.ShowDialog() != true)
                        return null;

                    var editedItem = editor.Result;

                    // --- DB連携する場合はここ ---
                    // UPDATE 処理
                    // SaveChanges() 等
                    // ----------------------------

                    return editedItem;
                },

                DeleteItem = item =>
                {
                    if (MessageBox.Show(
                            $"\"{item}\" を削除しますか?",
                            "確認",
                            MessageBoxButton.YesNo,
                            MessageBoxImage.Question)
                        != MessageBoxResult.Yes)
                    {
                        return;
                    }

                    // --- DB連携する場合はここ ---
                    // DELETE 処理
                    // SaveChanges() 等
                    // ----------------------------
                }
            };

            if (dlg.ShowDialog() == true)
            {
                var selected = dlg.SelectedItem;
                MessageBox.Show($"選択: {selected}");
            }
        };        
    }
}

実行例

起動

  1. 起動→ボタンを押す

    追加

  1. 「Add」を押す
  2. 追加要素を入力→OK
  3. リストビューに追加された様子

編集

  1. 要素を選択→「Edit」を押す
  2. 編集→OK
  3. リストビューが編集された様子

削除

  1. 要素を選択→「Delete」を押す
  2. 削除確認
  3. リストビューが削除された様子

構成

MyWpfApp
├─ App.xaml.cs
├─ MainWindow.xaml.cs
│
├─ Helpers
│     ├─ CollectionEditorDialog.cs
│     │   └─ 汎用・編集機能付きセレクター
│     │
│     ├─ UserEditorDialog.cs
│     │   └─ アプリ固有の編集UI
│     │
│     └─ (どのアプリにも再利用可能)
│
└─ MainWindow.xaml.cs

UserEditorDialogをHelpersに入れてしまっていますが、文字列なら動きますが、それ以外の場合は独自に作る必要がありますね。

コメント