10進数、16進数、8進数をコンボボックスで切り替えつつ入力チェックもしてくれる実装サンプル

あらすじ

こんな感じのものを実装しました。

f:id:bamch0h:20200720021840g:plain

XAML

<Window x:Class="WpfApp15.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:WpfApp15"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <ControlTemplate x:Key="ValidationTemplate">
            <StackPanel>
                <TextBlock Foreground="Red" Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=adornedelem}" />
                <AdornedElementPlaceholder x:Name="adornedelem" />
            </StackPanel>
        </ControlTemplate>
        <local:HexConverter x:Key="ByteHexConverter" />
    </Window.Resources>
    <StackPanel>
        <DataGrid ItemsSource="{Binding Items, Mode=OneWay}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Index" Binding="{Binding SelectedTypesIndex}" />
                <DataGridTemplateColumn IsReadOnly="True" Header="Types">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding Types}" SelectedIndex="{Binding SelectedTypesIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Number">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Margin="20" Width="100">
                                    <TextBlock.Text>
                                        <Binding Path="Number" Converter="{StaticResource ByteHexConverter}" />
                                    </TextBlock.Text>
                                </TextBlock>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox Margin="20" Width="100" Validation.ErrorTemplate="{StaticResource ValidationTemplate}">
                                <TextBox.Text>
                                    <Binding Path="Number" Converter="{StaticResource ByteHexConverter}" />
                                </TextBox.Text>
                            </TextBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
using Microsoft.Practices.Prism.Mvvm;
using System.Globalization;

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

            this.DataContext = new MainWindowVM();
        }
    }

    public class MainWindowVM : BindableBase
    {
        public List<DataGridItem> Items { get; set; }

        public MainWindowVM()
        {
            Items = new List<DataGridItem>()
            {
                new DataGridItem(123),
                new DataGridItem(234),
                new DataGridItem(0),
            };
        }
    }

    public class Number<T> : BindableBase
    {
        public Number(T initValue, string initFormat = "Decimal")
        {
            Value = initValue;
            Format = initFormat;
        }

        private T _value;
        public T Value
        {
            get
            {
                return _value;
            }

            set
            {
                this.SetProperty(ref this._value, value);
            }
        }

        private string format;
        public string Format
        {
            get
            {
                return format;
            }

            set
            {
                this.SetProperty(ref this.format, value);
            }
        }
    }

    public class DataGridItem : BindableBase
    {
        private Number<byte> number;
        public Number<byte> Number
        {
            get
            {
                return number;
            }

            set
            {
                this.SetProperty(ref this.number, value);
            }
        }

        public List<string> Types { get; set; }

        public int selectedTypesIndex;
        public int SelectedTypesIndex
        {
            get
            {
                return selectedTypesIndex;
            }

            set
            {
                this.SetProperty(ref this.selectedTypesIndex, value);
                if (this.Number != null)
                    Number = new Number<byte>(number.Value, Types[selectedTypesIndex]);
            }
        }

        public DataGridItem(byte initValue)
        {
            this.Types = new List<string>() { "Decimal", "Hexadecimal", "Octal" };

            this.Number = new Number<byte>(initValue, Types[SelectedTypesIndex]);

            this.SelectedTypesIndex = 0;
        }
    }

    public class HexConverter : IValueConverter
    {
        private Number<byte> byteNumber;
            
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            byteNumber = (Number<byte>)value;

            switch (byteNumber.Format)
            {
                case "Decimal":
                    return System.Convert.ToString(byteNumber.Value, 10);
                case "Hexadecimal":
                    return byteNumber.Value.ToString("X2");
                case "Octal":
                    return System.Convert.ToString(byteNumber.Value, 8);
                default:
                    return byteNumber.ToString();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value as string;
            byte resultValue;

            try
            {
                switch (byteNumber.Format)
                {
                    case "Hexadecimal":
                        resultValue = System.Convert.ToByte(strValue, 16);
                        return new Number<byte>(resultValue, byteNumber.Format);
                    case "Octal":
                        resultValue = System.Convert.ToByte(strValue, 8);
                        return new Number<byte>(resultValue, byteNumber.Format);
                    default:
                        resultValue = System.Convert.ToByte(strValue);
                        return new Number<byte>(resultValue, byteNumber.Format);
                }
            }
            catch
            {
                return DependencyProperty.UnsetValue;
            }
        }
    }
}

まとめ

VM側でConvertとValidationをするようにした場合、フォーカスが外れた後にValidationが走るのでエラーが見えないという欠点があってView側でやりたいという気持ちがあった。

View側でConvertを行うとformat単位でCoverterを切り替えたりできないので、Convert()メソッドでNumberクラスを受け取ってクラス内に定義してあるFormatを使って場合分けしている。CovertBackの場合は引数からNumberクラスが受け取れないので、Convert()メソッド時点のNumberクラスを保存しておいて、ConvertBackメソッドで使いまわしている。VM側で変更される場合は注意が必要かもしれないけど、編集中にVM上で変更されることはないはずなので問題ないはず。

ただ、今回はNumberというジェネリクスクラスを作成したけど、そこまでする必要はなかったかも。CoverterBackでフォーマット情報が取れないので、その保存方法としてNumberというクラスを作ったけど、formatをIMultiValueConverterで渡してあげて保存しておくだけでも今回の件は実現できたかもしれない。