WPFでUserControlを追加するだけでタブが増える自動タブシステム2

コンピュータ

AutoTab.cs

using System.Reflection;
using System.Windows.Controls;
using System.Globalization;
using System.Windows.Data;

namespace Maywork.WPF.AutoTab;

// =========================
// タブ定義用Attribute
// =========================

// UserControlに付与し、タブのメタ情報(ヘッダー・順序)を定義する
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class TabAttribute : Attribute
{
    public string Header { get; }   // タブに表示するヘッダー文字

    public int Order { get; }   // タブの表示順(小さい順)

    public TabAttribute(string header, int order)
    {
        Header = header;
        Order = order;
    }
}
/*
// 使用例:UserControlに付与するだけでタブとして認識される
[Tab("ホーム", 10)]
public partial class HomeTabView : UserControl
{
    public HomeTabView()
    {
        InitializeComponent();
    }
}
*/

// =========================
// タブ情報クラス
// =========================

// タブ生成に必要な情報をまとめたデータクラス
public class TabItemInfo
{
    public int Order { get; set; }  // 表示順

    public string Header { get; set; } = "";    // ヘッダー文字

    public Type ViewType { get; set; } = null!; // ビューの型
}

// =========================
// タブ情報ロード処理
// =========================

// リフレクションでUserControlを列挙し、TabAttribute付きのものだけ抽出する
public static class TabLoader
{
    public static List<TabItemInfo> LoadTabs()
    {
         // 実行中のアセンブリを取得
        var asm = Assembly.GetExecutingAssembly();

        // アセンブリ内の全型からタブ対象を抽出
        var tabs = asm.GetTypes()
            .Where(t =>
                t.IsClass &&        // クラスであること
                !t.IsAbstract &&    // 抽象クラスではない
                typeof(UserControl).IsAssignableFrom(t))    // UserControlか、その派生
            .Select(t => new
            {
                Type = t,   // 型
                Attribute = t.GetCustomAttribute<TabAttribute>()    // TabAttribute取得
            })
            .Where(x => x.Attribute is not null)    // 属性が付いているものだけ対象
            .Select(x => new TabItemInfo
            {
                Header = x.Attribute!.Header,   // 表示名
                Order = x.Attribute.Order,      // 並び順
                ViewType = x.Type               // インスタンス生成用の型
            })
            .OrderBy(x => x.Order)  // 第1キー:Order
            .ThenBy(x => x.Header)  // 第2キー:Header(ソート)
            .ToList();              // Listとして確定

        return tabs;
    }
}

// =========================
// 型 → インスタンス変換
// =========================

// ViewType (Type) を UserControl インスタンスに変換するコンバータ
// XAMLバインディング用
public class ViewTypeToInstanceConverter : IValueConverter
{
    public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Typeでなければ対象外
        if (value is not Type type)
            return null;
        
        // UserControlでなければ対象外
        if (!typeof(UserControl).IsAssignableFrom(type))
            return null;

        // リフレクションでインスタンス生成
        return Activator.CreateInstance(type) as UserControl;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // 逆変換は不要
        throw new NotSupportedException();
    }
}

ライブラリとして流用可能


MainWindowViewModel.cs

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

using Maywork.WPF.AutoTab;

namespace WpfSample01;

// =========================
// メインウィンドウ用ViewModel
// =========================
public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

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

    // 自動生成されたタブ情報のコレクション
    public ObservableCollection<TabItemInfo> Tabs { get; }

    // 現在選択されているタブ
    private TabItemInfo? _selectedTab;
    public TabItemInfo? SelectedTab
    {
        get => _selectedTab;
        set
        {
            if (_selectedTab == value) return;
            _selectedTab = value;
            OnPropertyChanged();
        }
    }

    // コンストラクタ
    public MainWindowViewModel()
    {
        // TabAttribute付きUserControlを自動検出してコレクション化
        Tabs = new ObservableCollection<TabItemInfo>(TabLoader.LoadTabs());
        // 初期選択:先頭タブを選択状態にする
        SelectedTab = Tabs.FirstOrDefault();
    }
}

MainWindow.xaml

<Window x:Class="WpfSample01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfSample01"
        xmlns:at="clr-namespace:Maywork.WPF.AutoTab"
        Title="AutoTab Demo" Height="200" Width="300">

    <Window.Resources>
        <at:ViewTypeToInstanceConverter x:Key="ViewTypeToInstanceConverter"/>
    </Window.Resources>

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

    <Grid>
        <TabControl ItemsSource="{Binding Tabs}"
                    SelectedItem="{Binding SelectedTab}">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Header}" />
                </DataTemplate>
            </TabControl.ItemTemplate>

            <TabControl.ContentTemplate>
                <DataTemplate>
                    <ContentControl Content=
                        "{Binding ViewType, Converter={StaticResource ViewTypeToInstanceConverter}}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</Window>

TabControlはListViewなどと同様にItemsControlを継承したコントロールです。

そのため、ItemsSourceやSelectedItemはListViewと同じようにデータバインディングで扱うことが出来ます。

また、DataTemplateを利用することで、各タブの表示内容(HeaderやContent)を柔軟に定義することが出来ます。

本サンプルでは、DataTemplate内でConverterを使用し、型情報(Type)からUserControlのインスタンスを生成しています。


以下タブに表示されるUserControlです。
追加すると自動的にタブが増えます。

HomeTabView.xaml

<UserControl x:Class="WpfSample01.Views.HomeTabView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             MinWidth="200"
             MinHeight="100">
    <Grid Margin="16">
        <StackPanel>
            <TextBlock FontSize="24" Text="Home" />
            <TextBlock Margin="0,8,0,0" Text="これはホームタブです。" />
            <Button Width="140" Margin="0,16,0,0" Content="ボタン" />
        </StackPanel>
    </Grid>
</UserControl>

HomeTabView.xaml.cs

using System.Windows.Controls;

using Maywork.WPF.AutoTab;

namespace WpfSample01.Views;

[Tab("ホーム", 10)]
public partial class HomeTabView : UserControl
{
    public HomeTabView()
    {
        InitializeComponent();
    }
}

コメント