ポンコツXAMLer奮闘記

C#(主にXAML)関連のメモ書きがメインです。

入力チェックの簡略化(コード紹介)

前回は単純な入力チェック機能を持ったクラスの概念を説明しました。

nakasato-work.hatenablog.com

今回は、実際にコードの落とした場合の説明をします。
というか、ライブラリ化してGitHubに乗せたので、ダウンロードしましょう。

github.com

NCoreプロジェクトのValue配下がバリュークラス一式です。

AGenValueが抽象化された大本のクラスです。

その他、CGen~と命名されたクラスが、実際の値を持つクラスになります。

基本的に、ベースクラスに処理を詰め込まず、継承して機能を追加しましょう!

昔々、あるところに、同じ思想を抱いた方がいましたが、何を思ったのか、ベースクラスに集約しよう!とか言って、以下のような大罪を犯してしまいました。

class CValue
{
  public EDataType DataType;
  public int IntValue = 0;
  public double DoubleValue = 0;
  public string StringValue = "";
  public TimeSpan TimeValue;

  public bool SetValue(EDataType dataType, string str)
  {
    bool success = false;

    switch(dataType)
    {
      case eDouble:
        success = double.TryParse(str, out this.DoubleValue);
        break;
      case eInt:
        success = int.TryParse(str, out this.IntValue);
        break;
      case eString:
        this.StringValue = str;
        success = true;
        break;
      case eTime:
        success = TimeSpan.TryParse(str, out this.TimeValue);
        brea;
      default: //何もしない
        break;
    }

    return success;
  }
}

class CTimeValue : CValue
{
  public CTimeValue()
  {
    this.DataType = EDataType.eTime;
  }
}

ぱっと見問題ないように見えるこちらのソース。
よくよく考えると、非常によくないです。
あなたにこのソースの問題点がわかるでしょうか?
これの問題点がわかると、あなたのOPPレベルは格段に向上するでしょう。
(ほんと、これにすごい苦しみを与えられました。。。)

・継承による拡張性
・処理はあるべきクラスに配置する

という原則を守らないと、規模が大きくなったときに、トレースが大変になるので注意が必要です。
コアな部分は後で変更するのは難しいものです。
どこまでをベースクラスに置いて、どこまでを委譲するかは経験と嗅覚とゲーマーの感にゆだねられます。
(この辺がOPP指向の難しいところで、日本人がビビッて使いたがらないところの原因ですかね)


さて、話を戻しましょう。


実は上で紹介したGitHubリポジトリに、デモアプリも入っています。
NCoreDemoがそれですね。

使い方はいたって簡単!

まずはViewModelを以下のように定義します。

class CViewModel : BindableBase
{
    protected CGenDec _ItemValue = new CGenDec("ItemValue", "ItemValue", "%", 0, -10, 110, 1, true);

    public CGenDec ItemValue
    {
        get { return this._ItemValue; }
        set { this.SetProperty(ref this._ItemValue, value); }
    }

    protected CGenStr _ItemName = new CGenStr("ItemName", "ItemName", "input here!", 0, 10, true);

    public CGenStr ItemName
    {
        get { return this._ItemName; }
        set { this.SetProperty(ref this._ItemName, value); }
    }

    public CViewModel()
    {
        ItemName.ShowMessage += ItemName_ShowMessage;
        ItemValue.ShowMessage += ItemValue_ShowMessage;
    }

    private void ItemName_ShowMessage(AGenValue arg1, Exception arg2)
    {
        MessageBox.Show(arg2.Message);
    }

    private void ItemValue_ShowMessage(AGenValue arg1, Exception arg2)
    {
        MessageBox.Show(arg2.Message);
    }
}

次に、XAMLバインドします。

<Label Content="{Binding ItemName.Name}" IsEnabled="{Binding IsItemEnable.Value}"/>
<TextBox Text="{Binding ItemName.Text}" IsEnabled="{Binding IsItemEnable.Value}"/>
<Label Content="{Binding ItemValue.Name}" IsEnabled="{Binding IsItemEnable.Value}"/>
<TextBox Text="{Binding ItemValue.Text}" IsEnabled="{Binding IsItemEnable.Value}"/>

これだけで入力チェックができて、メッセージボックスまで表示できてしまいます!
簡単でしょ?

今回はここまでとします。
よいOPP生活を!

入力チェックの簡略化(概念定義)

仕事が忙しく、なかなか更新できない日々です。

今回はアプリを作る上で必ず発生する、ユーザー入力チェックを簡略化する方法を紹介します。

これを知れば、いままでイベントハンドラに入力チェックをガリガリ書いていたのが馬鹿らしくなります。

WindowsForm時代によくやっていた方法は以下ですね。
(例が微妙かもしれませんが。。。)

int _Number;

//LostFocusイベントハンドラ
private void TextBox1_LostFocus(object sender, EventArgs e)
{
    TextBox text = (TextBox)sender;
    
    try
    {
        int tempNum;
        tempNum = int.Parse(text.Text);

        // 0以上、100以下だったらOK、他はNG
        if((0 > tempNum) && (100 < tempNum))
        {
            throw new Exception("範囲外エラー");
        }

        this._Number = tempNum; 
    }
    catch(Exception ex)
    {
        MessageBox.Show(ex.Message);
        
        text.Text = this._Number.ToString();
    }
}

しかし、これだと、項目が増えるたびにイベントハンドラを用意してあげないといけません。
単純ですが、増えれば増えるほどしんどいし、無駄な工数になります。

さて、どうしたものか。。。いろいろ考えました。
行き着いた答えは、「入力チェック機構を持ったバリュークラスを作ってしまえ!」でした。

クラス図で概要を表すと以下のようになります。

f:id:nakasato-work:20161201125559p:plain

考えとしては、

  1. テキストボックスとバインディングするだろうから、string型を窓口にしよう!(Textプロパティ)
  2. 実際の値の型に対する処理はサブクラスに委譲してしまえ!(サブクラス側でしかValueプロパティを持たない)
  3. 文字列なら文字数、数値型なら範囲が単純な入力チェックの処理だよね!共通!(バリュークラスにMin・Maxプロパティを持つ)
  4. リュークラスの中でエラーダイアログを表示させると、後々めんどくさいことになるから、メッセージ表示は上位に委譲してしまえ!(ShowMessageイベントハンドラ)

大体こんな感じです。とりあえず、今回はここまでとします。
次回は、具体的な実装方法を紹介します。

言語切替機能を実装する(任意のロケールIDを指定できる)

だいぶ時間が空いてしまいました。。。

前回はOSのデフォルトロケールIDに従って言語リソースを入れ替える方法を紹介しました。

nakasato-work.hatenablog.com

今回は、任意のロケールIDを指定することで、言語リソースを入れ替える方法を紹介します。

NuGetで、「Prism.Mvvm」をインストールしてください。(BindableBaseを使用する為)

次に、以下のクラスを定義します。

namespace TestApp
{
    public class CResourceService : BindableBase
    {

        #region singleton members

        private static readonly CResourceService _current = new CResourceService();
        public static CResourceService Current
        {
            get { return _current; }
        }

        #endregion

        private readonly TestApp.Properties.Resources _resources = new Resources();

        /// <summary>
        /// 多言語化されたリソースを取得します。
        /// </summary>
        public TestApp.Properties.Resources Resources
        {
            get { return this._resources; }
        }

        /// <summary>
        /// 指定されたカルチャ名を使用して、リソースのカルチャを変更します。
        /// </summary>
        /// <param name="name">カルチャの名前。</param>
        public void ChangeCulture(string name)
        {
            Resources.Culture = CultureInfo.GetCultureInfo(name);
            this.OnPropertyChanged("Resources");
        }
    }
}

まず、Resourcesプロパティがカレントの言語リソースです。

ChangeCultureでロケールID("ja"や"de"等)を渡すと、Resourcesの言語リソースが切り替わります。

ロケールIDの一覧はロケール ID (LCID) の一覧を参照してください。

使い方は以下です。

CResourceService.Current.ChangeCulture("ja");

コンボボックス等で言語リソースを切り替えたい場合、key=コンボボックス表示文字列、Value=ロケールIDの連想配列を作成しておくと、簡単に言語リソースを切り替えることができます。

Dictionary<string, string> dict = new Dictionary<string, string>();

dict["日本語"] = "ja";
dict["英語"] = "en";
dict["中国語(シンガポール)"] = "zh-sg";

こんな感じの連想配列ですね。

さぁ、ここまで来たら、後はGUIバインドするだけです!!(長かった。。。)

前回紹介したXAMLを元に修正すると、以下のようになります。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:app="clr-namespace:TestApp"

    <-! 省略 -->

    <Button Content="{Binding Source={x:Static app:CResourceService.Current}, Path=Resources.[リソース名], Mode=OneWay}"/>

ちょっとContentのバインド指定が長くなってしまいます。。。(^^;)

これで任意のロケールIDを指定して言語リソースを切り替えることができるようになりました!

今回、序盤に登場したPrismについては全く触れませんでした。

しかし、このPrismはXAMLを征する上でかなり重要な役割を果たしてくれます。
数年前に比べてPrismに関するネット記事も増えているので、是非とも検索してみてください。

言語切替機能を実装する(デフォルトロケールID連動版)

日本の特定ユーザー向け業務アプリだと日本語オンリーでも十分ですが、海外で販売したり、販売先は国内ですが、国外エンジニアがアプリを使用するというケースがあります。

そういったユーザーの場合、「日本語と英語を切り替えたい」なーんてことをサラッと言われたりします。

そんな時どうすればよいのか?という小技を紹介したいと思います。

基本的には、以下のことを行います。

  1. 対応する言語の数だけサテライトライブラリを用意する。
  2. WindowsのOSの言語、またはユーザーに言語を選択させることで、サテライトライブラリの参照先を変更する。

サテライトライブラリとは、DLL形式の言語リソースファイルのことです。
(VisualStudioで生成すると、大体は「~.resources.dll」のような名前で生成されます。)

VisualStudioでC#プロジェクトを生成すると、以下のようなツリーになっていると思います。

[プロジェクト名]
   Properties
        AssemblyInfo.cs
        Resources.resx
        Settings.settings

上記のツリーの「Resources.resx」がデフォルトのリソースファイルです。
このファイルひとつが言語ひとつ分に相当します。

例えば、英語リソースをデフォルトリソース、日本語をオプションリソースとする場合、「Resources.resx」に英語文字列を記述し、「Resources.ja.resx」に日本語文字列を記述します。

お気付きになられたでしょうか?

つまりは、オプションリソースを追加する場合、Resouces.[ロケールID].resxというファイルを作成し、作成したファイルに言語別の文字列を登録すればよいわけですね。

[プロジェクト名]
   Properties
        AssemblyInfo.cs
        Resources.da.resx       ・・・デンマーク語用
        Resources.ja.resx    ・・・日本語用
        Resources.resx     ・・・英語用
        Settings.settings

ロケールIDとは、Windows内部で定義されている、言語別のID文字列のことです。
詳しくは ロケール ID (LCID) の一覧 を参照してください。

さて、ここまで来たら次は実際にコントロールに表示してみましょう。
ボタンに表示する文字列をサテライトライブラリから取得した場合の例を以下に示します。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:prop="clr-namespace:[プロジェクト名].Properties"

    <-! 省略 -->

    <Button Content="{x:Static prop:Resources.[リソース名]}"/>

はい。簡単です。
[プロジェクト名]と[リソース名]を実際のものに入れ替えるだけで、リソース登録した文字列が使用できます。

これだけで、OSのデフォルトロケールIDと一致するサテライトライブラリを自動で検出して、文字列が入れ替わってくれます!
(あら便利!)

「これで、コンパネの地域と言語で表示を切り替えながらデバッグできる!(≧▽≦)」
なーんて思ってる方。。。残念!!!

ここでWindowsの嫌がらせが発動します。。。

OSのデフォルトロケールIDという単語(私が勝手にネーミング)に注目です。
これは、コンパネの地域と言語では変わりません。

日本語版Windowsとか、中国版Windowsとか、英語版Windowsなんて言葉を聞いたことありませんか?
インストールしたOSの言語によって、システムが出すダイアログの文言が変わるのはご存知でしょうか?
実はこれ、インストールしたOSの言語によって変わります。。。
(私の認識だとこうなんですが、有識者の方、間違ってたらご指摘ください)


「え??ってことは、自分でサテライトライブラリを選べないの?(# ゚Д゚)」

いいえ、そんなことはありません。
TortoiseSVN等、任意の言語に切替できるアプリもちゃんとあります。

次回は、任意に言語切替する方法を紹介したいと思います。

XMLファイルを簡単に読み書きしたい

アプリケーションを作っていると、設定ファイルを残す場面が必ずありますね。
でも、主要機能じゃないからあまり時間をかけられない。
でもでもー、CSVファイルとかにすると、自分でパーサーをガリガリ書かなきゃいけなくなる。。。
どうしたものか。。。

そんなときはXMLファイルです。
.NetのXMLパーサーは割と賢くなっていて、ガリガリコーディングしなくても、XMLのツリー構造を構築・逆にデータを再現することができます。
データが巨大になったり、バージョンアップでコロコロ構造が変わる場合はお勧めできないですがね。。。

その辺のトレードオフはご自身の判断でお願いします。


今回は下のXMLファイルを読み書きすることにします。

<?xml version="1.0" encoding="utf-8"?>
<Settings>
  <MyName>ポンコツ太郎</MyName>
  <MyAge>26</MyAge>
  <MyAddr>日本のスクラップ山</MyAddr>
  <MyAddrList>
    <AddrItem>
      <Name>メリケン</Name>
      <Age>10</Age>
      <Addr>アメリカ</Addr>
    </AddrItem>
    <AddrItem>
      <Name>ジョーカー</Name>
      <Age>17</Age>
      <Addr>四茶</Addr>
    </AddrItem>
  </MyAddrList>
</Settings>

まずは、XMLファイルのタグと一対一になるクラスを定義します。

[XmlRoot("Settings")]
public class CSettingsXml
{
    /// <summary>
    /// 自分の名前
    /// </summary>
    [XmlElement("MyName")]
    public string MyName = "";

    /// <summary>
    /// 自分の年齢
    /// </summary>
    [XmlElement("MyAge")]
    public int MyAge = 0;

    /// <summary>
    /// 自分の住所
    /// </summary>
    [XmlElement("MyAddr")]
    public string MyAddr = "";

    /// <summary>
    /// 自分の連絡帳
    /// </summary>
    [XmlArray("MyAddrList")]
    [XmlArrayItem("AddrItem")]
    public List<CAddrItem> MyAddrList = new List<CAddrItem>();

    /// <summary>
    /// XMLファイルを読み込む
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    public static CSettingsXml Load(string path)
    {
        CSettingsXml loaddata;

        using (var sr = new StreamReader(path, new System.Text.UTF8Encoding(false)))
        {
            loaddata = (CSettingsXml)serializer.Deserialize(sr);
            sr.Close();
        }

        return loaddata;
    }

    /// <summary>
    /// XMLファイルに保存する
    /// </summary>
    /// <param name="path"></param>
    public void Save(string path)
    {
        using (var sw = new StreamWriter(path, false, new System.Text.UTF8Encoding(false)))
        {
            var ns = new XmlSerializerNamespaces();
            ns.Add(String.Empty, String.Empty);
            serializer.Serialize(sw, this, ns);
            sw.Close();
        }
    }
}

public class CAddrItem
{
    /// <summary>
    /// 名前
    /// </summary>
    [XmlElement("Name")]
    public string Name = "";

    /// <summary>
    /// 年齢
    /// </summary>
    [XmlElement("Age")]
    public int Age = 0;

    /// <summary>
    /// 住所
    /// </summary>
    [XmlElement("Addr")]
    public string Addr = "";
}

これでCSettingsXmlのインスタンスでSave()を呼ぶと保存。CSettingsXml.Load()でデータの再現ができます。

どうです?簡単でしょ?

うーん、でも、これだと、XMLファイルがいっぱいできたときに全部のクラスにSave()とLoad()を実装しなきゃなりません。

せっかくのオブジェクト指向言語ですからね。カッコよくしましょう。


以下のクラスを追加してください。これを見たらもうお分かりになるはず。。。

public abstract class AXMLSerializer<T>
{
    protected static readonly XmlSerializer serializer = new XmlSerializer(typeof(T));

    /// <summary>
    /// XMLファイルを読み込む
    /// </summary>
    /// <param name="path"></param>
    /// <returns></returns>
    public static T Load(string path)
    {
        T loaddata;

        using (var sr = new StreamReader(path, new System.Text.UTF8Encoding(false)))
        {
            loaddata = (T)serializer.Deserialize(sr);
            sr.Close();
        }

        return loaddata;
    }

    /// <summary>
    /// XMLファイルに保存する
    /// </summary>
    /// <param name="path"></param>
    public void Save(string path)
    {
        using (var sw = new StreamWriter(path, false, new System.Text.UTF8Encoding(false)))
        {
            var ns = new XmlSerializerNamespaces();
            ns.Add(String.Empty, String.Empty);
            serializer.Serialize(sw, this, ns);
            sw.Close();
        }
    }
}

そして、CSettingsXmlを改造します。

[XmlRoot("Settings")]
public class CSettingsXml : AXMLSerializer<CSettingsXml>
{
    /// <summary>
    /// 自分の名前
    /// </summary>
    [XmlElement("MyName")]
    public string MyName = "";

    /// <summary>
    /// 自分の年齢
    /// </summary>
    [XmlElement("MyAge")]
    public int MyAge = 0;

    /// <summary>
    /// 自分の住所
    /// </summary>
    [XmlElement("MyAddr")]
    public string MyAddr = "";
}

何が変わったかというと、Save()とLoad()をCSettingsXmlからAXMLSerializerに移動。
そして、CSettingsXmlをAXMLSerializerのサブクラスにしたわけですね。
こうすると、XMLが増えてもAXMLSerializerを継承すれば、すぐにSave()とLoad()が使えるようになります。

つくしぃ。。。

しかし、public static T Load(string path)を見て分かる通り、Tとはなんぞや?と思われた方もいるでしょう。
これが噂のジェネリック型というやつです。(※医薬品ではありません)

要は、キャスト無しで型を選べるようになるということです。

はるか昔、C言語が繁栄したときは、

func(int type, char *param)
{
    if(1 == type)
    {
        func2((long *)param);
    }
    else if(2 == type)
    {
        func3((int *)param);
    }
    else
    {
        func4((short *)param);
    }
}

のようにしてパラメータを汎用化していた頃がありました。
(普通はparamの型は構造体かな?ちょっと例が悪いですが。。。)

でもジェネリックを使うと、

public class CSettingsXml : AXMLSerializer<CSettingsXml>

と定義するだけで、Tの部分がCSettingsXmlに置き換わり、キャストが不要になるのです!

詳細はジェネリックで検索、検索~☆

今回はここまでにしましょう。

bool型の変数値を反転させて、コントロールにバインディングしたいとき

たとえば、ONボタンとOFFボタンをトグルさせたい場合があります。

ONボタンのIsEnabledとOFFボタンのIsEnabledが反転するようにすればいいのですが、どうすればよいのでしょうか?

答えは簡単です。Converterを使用するのです。

<!--GUIのXAML-->
<Button x:Name="BtnOn" Content="ON" IsEnabled="True"/>
<Button x:Name="BtnOff" Content="OFF" IsEnabled="{Binding ElementName=BtnOn, Path=IsEnabled, Converter={StaticResource BoolNeg}}"/>

Converterを指定すると、テキストで入力した文字列をEnum型やBool型に変換したり、その逆もできてしまいます。
今回は単純にTrue/Falseを反転させるだけの簡単なものなので、Converterは以下のようになります。

/// <summary>
/// GUIのバインディングで、bool値を反転してくれるコンバーター
/// </summary>
public class CBoolNegativeConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return !(value is bool && (bool)value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return !(value is bool && (bool)value);
    }

}

また、Converter={StaticResource BoolNeg}のように指定していますので、例えば、どのソースからでも参照できるようにApp.xamlに以下のように追記します。

<Application x:Class="BoolNegTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:BoolNegTest">
    <Application.Resources>
        <ResourceDictionary>
            <local:CBoolNegativeConverter x:Key="BoolNeg"/>
        </ResourceDictionary>
    </Application.Resources>
</Application>

これでBoolNegがStaticResourceに登録されるので、GUIXAMLでエラーが出なくなります。

後は、ONボタン、OFFボタンのクリックイベントハンドラを実装し、押されたボタンによって、ONボタンのIsEnabledを変更すればONボタンとOFFボタンのトグルができるようになります。

  1. OKボタンクリック -> IsEnabled = False
  2. OFFボタンクリック -> IsEnabled = True

今回はここまでです。
このbool型の反転バインドができるだけで、世界が変わります。