20190727のC#に関する記事は4件です。

連想配列の基本操作【JavaScript/C#/VB/Python/CommonLisp/HSP】

最近いくつかの言語の連想配列の操作を調査したのでそれをここにまとめておく。

JavaScript

JavaScriptで連想配列を使うには昔ながらのオブジェクトを使ったものとES6で追加されたMapクラスを使う方法があります。

Object

"use strict";

//初期化
const kantoh={
    "茨城県":"水戸市",
    "栃木県":"宇都宮市",
    "群馬県":"前橋市",
    "埼玉県":"さいたま市",
    "神奈川県":"横浜市"
};
//代入
kantoh["千葉県"]="千葉市";
//要素の有無
console.log("千葉県" in kantoh);
//追加と代入は同じ
kantoh["東京都"]="新宿区";
//要素が登録済みなら代入したくない場合
if(!("東京都" in kantoh)){
    kantoh["東京都"]="新宿区";
}
else{
    console.log("同一のキーを含む項目が既に追加されています。");
}
//取得
console.log(kantoh["千葉県"]);
//代入演算子も使用可
kantoh["千葉県"]+="中央区";
console.log(kantoh["千葉県"]);
//要素数
console.log(Object.keys(kantoh).length);
//要素の削除
delete kantoh["千葉県"];
//キーの配列
console.log(Object.keys(kantoh).join(","));
//値の配列
console.log(Object.values(kantoh).join(","));
//ループ
for(let [key,value] of Object.entries(kantoh)){
    console.log(`key:${key} value:${value}`);   
}
//Join
const joinDict=
    dict=>Object.entries(dict).map(v=>v.join(":")).join(", ");
console.log(joinDict(kantoh));
//要素のクリア
for(let i of Object.keys(kantoh)) delete kantoh[i];
kantoh["なし"]="なにもないよ";
console.log(joinDict(kantoh));

Map

"use strict";

//初期化
const kantoh=new Map([
    ["茨城県","水戸市"],
    ["栃木県","宇都宮市"],
    ["群馬県","前橋市"],
    ["埼玉県","さいたま市"],
    ["神奈川県","横浜市"]
]);
//代入
kantoh.set("千葉県","千葉市");
//要素の有無
console.log(kantoh.has("千葉県"));
//追加と代入は同じ
kantoh.set("東京都","新宿区");
//要素が登録済みなら代入したくない場合
if(!kantoh.has("東京都")){
    kantoh.set("東京都","新宿区");
}
else{
    console.log("同一のキーを含む項目が既に追加されています。");
}
//取得
console.log(kantoh.get("千葉県"));
//代入演算子は使用不可
const resetAdd=(dict,key,value)=>dict.set(key,dict.get(key)+value);
resetAdd(kantoh,"千葉県","中央区");
console.log(kantoh.get("千葉県"));
//要素数
console.log(kantoh.size);
//要素の削除
kantoh.delete("千葉県");
//キーの配列
console.log(Array.from(kantoh.keys()));
//値の配列
console.log(Array.from(kantoh.values()));
//ループ
for(let [key,value] of kantoh){
    console.log(`key:${key} value:${value}`);   
}
//Join
const joinDict=
    dict=>Array.from(dict).map(v=>v.join(":")).join(", ");
console.log(joinDict(kantoh));
//要素のクリア
kantoh.clear();
kantoh.set("なし","なにもないよ");
console.log(joinDict(kantoh));

Mapは最近新しく追加された機能であるだけあってメソッドが綺麗に整備されています。
しかし、初期化や取得、代入リテラルがオブジェクトに比べて少し不自然なのが辛いところ。

Python

#初期化
kantoh={
    "茨城県":"水戸市",
    "栃木県":"宇都宮市",
    "群馬県":"前橋市",
    "埼玉県":"さいたま市",
    "神奈川県":"横浜市"
}
#代入
kantoh["千葉県"]="千葉市"
#要素の有無
print("千葉県" in kantoh)
#追加と代入は同じ
kantoh["東京都"]="新宿区"
#要素が登録済みなら代入したくない場合
if "東京都" not in kantoh:
    kantoh["東京都"]="新宿区"
else:
    print("同一のキーを含む項目が既に追加されています。")
#取得
print(kantoh["千葉県"])
#代入演算子も使用可
kantoh["千葉県"]+="中央区"
print(kantoh["千葉県"])
#要素数
print(len(kantoh))
#要素の削除
del kantoh["千葉県"]
#キーの配列
print(list(kantoh.keys()))
#値の配列
print(list(kantoh.values()))
#ループ
for key,value in kantoh.items():
    print(f"key:{key} value:{value}")
#Join
joinDict=(
    lambda dict:",".join([":".join(v) for v in dict.items()]))
print(joinDict(kantoh))
#要素のクリア
kantoh.clear()
kantoh["なし"]="なにもないよ"
print(joinDict(kantoh))

Pythonのdictは言語レベルで組み込まれていてリテラルも何も不自由なく使用できます。

C#

using System;
using System.Collections.Generic;
using System.Linq;

class Program{
    static void Main(){
        //初期化
        var kantoh=new Dictionary<string,string>{
            {"茨城県","水戸市"},
            {"栃木県","宇都宮市"},
            {"群馬県","前橋市"},
            {"埼玉県","さいたま市"},
            {"神奈川県","横浜市"}
        };
        //代入
        kantoh["千葉県"]="千葉市";
        //要素の有無
        Console.WriteLine(kantoh.ContainsKey("千葉県"));
        //追加
        kantoh.Add("東京都","新宿区");
        //要素が登録済みの場合、ArgumentException
        try{
            kantoh.Add("東京都","新宿区");
        }
        catch(ArgumentException ex){
            Console.WriteLine(ex.Message);
        }
        //取得
        Console.WriteLine(kantoh["千葉県"]);
        //代入演算子も使用可
        kantoh["千葉県"]+="中央区";
        Console.WriteLine(kantoh["千葉県"]);
        //要素数
        Console.WriteLine(kantoh.Count);
        //要素の削除
        kantoh.Remove("千葉県");
        //キーの配列
        Console.WriteLine(string.Join(",",kantoh.Keys));
        //値の配列
        Console.WriteLine(string.Join(",",kantoh.Keys));
        //ループ
        foreach(var v in kantoh){
            Console.WriteLine($"key:{v.Key} value:{v.Value}");
        }
        //Join
        Func<Dictionary<string,string>,string> joinDict=
            dict=>string.Join(", ",dict.Select(v=>v.Key+":"+v.Value));
        Console.WriteLine(joinDict(kantoh));  
        //要素のクリア
        kantoh.Clear();
        kantoh["なし"]="なにもないよ";
        Console.WriteLine(joinDict(kantoh));  
    }
}

極々普通な辞書。静的言語なので型定義はしておく必要があります。

VB

Option Strict On
Option Infer On
Imports System.Collections.Generic
Imports System.Linq

Module Program
    Sub Main()
        '初期化
        Dim kantoh As New Dictionary(Of String,String) From {
            {"茨城県","水戸市"},
            {"栃木県","宇都宮市"},
            {"群馬県","前橋市"},
            {"埼玉県","さいたま市"},
            {"神奈川県","横浜市"}
        }
        '代入
        kantoh("千葉県")="千葉市"
        '要素の有無
        Console.WriteLine(kantoh.ContainsKey("千葉県"))
        '追加
        kantoh.Add("東京都","新宿区")
        '要素が登録済みの場合、ArgumentException
        Try
            kantoh.Add("東京都","新宿区")
        Catch ex As ArgumentException
            Console.WriteLine(ex.Message)
        End Try
        '取得
        Console.WriteLine(kantoh("千葉県"))
        '代入演算子も使用可
        kantoh("千葉県")+="中央区"
        Console.WriteLine(kantoh("千葉県"))
        '要素数
        Console.WriteLine(kantoh.Count)
        '要素の削除
        kantoh.Remove("千葉県")
        'キーの配列
        Console.WriteLine(String.Join(",",kantoh.Keys))
        '値の配列
        Console.WriteLine(String.Join(",",kantoh.Keys))
        'ループ
        For Each v In kantoh
            Console.WriteLine($"key:{v.Key} value:{v.Value}")
        Next
        'Join
        Dim joinDict As Func(Of Dictionary(Of String,String),String)=
            Function(dict) String.Join(", ",dict.Select(Function(v) v.Key & ":" & v.Value))
        Console.WriteLine(joinDict(kantoh))
        '要素のクリア
        kantoh.Clear()
        kantoh("なし")="なにもないよ"
        Console.WriteLine(joinDict(kantoh))
    End Sub
End Module

C#と同じ。

HSP

HSPの言語機能に連想配列なんてものは存在しないので
以前自作した連想配列モジュール「DictionaryOnHSP」を使用します。
大体.NET(C#,VB)っぽい命令/関数群が用意されています。

#runtime "hsp3cl"
#cmpopt varinit 1
#include "Dictionary.as"

;初期化
new@Dictionary kantoh,"str",,,{"
    茨城県:水戸市,
    栃木県:宇都宮市,
    群馬県:前橋市,
    埼玉県:さいたま市,
    神奈川県:横浜市
"}
;代入
dcSet kantoh,"千葉県","千葉市"
;要素の有無
mes dcContainsKey(kantoh,"千葉県")
;追加
dcAdd kantoh,"東京都","新宿区"
;要素が登録済みの場合、stat1が返り何もしない
dcAdd kantoh,"東京都","新宿区"
if 1=stat{
    mes "同一のキーを含む項目が既に追加されています。"
}
;取得
mes dcItem(kantoh,"千葉県")
;代入演算子っぽい何かも使用可
dcReSet kantoh,"千葉県","+","中央区"
mes dcItem(kantoh,"千葉県")
;要素数
mes dcCount(kantoh)
;要素の削除
dcRemove kantoh,"千葉県"
;キーの配列
sdim keys: dcRefKeys kantoh,keys
mes dcJoinArray(keys,",")
;値の配列
sdim values: dcRefValues kantoh,values
mes dcJoinArray(values,",")
;ループ
dcForeach kantoh
    mes strf("key:%s value:%s",dcKeys(kantoh,cnt),dcValues(kantoh,cnt))   
loop
;Join
mes dcJoinDict(kantoh,":",",")   
;要素のクリア
dcClear kantoh
dcSet kantoh,"なし","なにもないよ"
mes dcJoinDict(kantoh)

CommonLisp

全体的にかなり冗長。
連想リストという選択肢もあります。

(let(
        ;初期化
        (kantoh (make-hash-table :test #'equal)))
    ;初期化は愚直
    (setf
        (gethash "茨城県" kantoh) "水戸市"
        (gethash "栃木県" kantoh) "宇都宮市"
        (gethash "群馬県" kantoh) "前橋市"
        (gethash "埼玉県" kantoh) "さいたま市"
        (gethash "神奈川県" kantoh) "横浜市")
    (format t "~A~%" (gethash "千葉県" kantoh))
    ;代入
    (setf (gethash "千葉県" kantoh) "千葉市")
    ;要素の有無 gethashの返り値の2価を
    (format t "~A~%" (multiple-value-list(gethash "千葉県" kantoh)))
    ;追加と代入は同じ
    (setf (gethash "東京都" kantoh) "新宿区")
    ;要素が登録済みなら代入したくない場合
    (if (not(cadr(multiple-value-list(gethash "東京都" kantoh))))
        (setf (gethash "東京都" kantoh) "新宿区")
        (write-line "同一のキーを含む項目が既に追加されています。"))
    ;取得
    (format t "~A~%" (gethash "東京都" kantoh))
    ;代入演算子なんてない
    (setf (gethash "千葉県" kantoh)
        (concatenate 'string (gethash "千葉県" kantoh) "中央区"))
    ;要素数
    (format t "~d~%" (hash-table-count kantoh))
    ;要素の削除
    (remhash "千葉県" kantoh)
    ;キーの配列
    (format t "~A~%" (loop for key being the hash-keys in kantoh collect key))
    ;値の配列
    (format t "~A~%" (loop for key being the hash-keys in kantoh using (hash-value value) collect value))
    ;ループ
    (maphash #'(lambda(key value)
            (format t "key:~A value:~A~%" key value))
        kantoh)
    ;Join
    (labels((joinDict(dict)
            (format nil "~{~A~^,~}" 
                (mapcar #'(lambda(item) (concatenate 'string (car item) ":"  (cadr item)))
                    (loop for key being the hash-keys in kantoh using (hash-value value) collect
                        (list key value))))))
        (format t "~A~%" (joinDict kantoh))
        ;要素のクリア
        (maphash #'(lambda(key value) (remhash key kantoh)) kantoh)
            (setf (gethash "なし" kantoh) "なにもないよ")
            (format t "~A~%" (joinDict kantoh))))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

画面表示(スクリーンショット)から、QRコードを読み込んで、デコードするC#の単体アプリです

画面表示(スクリーンショット)から、QRコードを読み込んで、デコードする

C#の単体アプリです

※同様の機能のフォームアプリはこちら
https://qiita.com/santarou6/items/d1090338def65f3af5bc

screen_qr_decode.cs
//c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /reference:QRCodeEncoderDecoderLibrary.dll /target:winexe screen_qr_decode.cs

using System;
using System.Windows.Forms;
using System.Drawing;
using QRCodeEncoderDecoderLibrary;

public class screen_qr_decode
{
 public static void Main(string[] args)
 {
    try{
    QRDecoder   QRCodeDecoder;
    Bitmap      QRCodeInputImage;
    QRCodeDecoder = new QRDecoder();
    QRCodeInputImage = new Bitmap(Screen.PrimaryScreen.Bounds.Width,Screen.PrimaryScreen.Bounds.Height);

    Graphics g = Graphics.FromImage(QRCodeInputImage);
    g.CopyFromScreen(new Point(0, 0), new Point(0, 0), QRCodeInputImage.Size);
    g.Dispose();

    QRCodeInputImage.Save("temporary.png", System.Drawing.Imaging.ImageFormat.Png);

    byte[][] DataByteArray = QRCodeDecoder.ImageDecoder(QRCodeInputImage);

    //string Result = System.Text.Encoding.GetEncoding(932).GetString(DataByteArray[0]);
    string Result = System.Text.Encoding.UTF8.GetString(DataByteArray[0]);
    //string Result = System.Text.Encoding.GetEncoding(51932).GetString(DataByteArray[0]);

    MessageBox.Show(Result);

    }
    catch(Exception ex){
        MessageBox.Show("読み取りエラー\n\n"+ex);
    }

 }

}

実行例
キャプチャ.PNG

※ライブラリは以下参照
QR Code Encoder and Decoder .NET Class Library
https://www.codeproject.com/Articles/1250071/QR-Code-Encoder-and-Decoder-NET-Class-Library-Writ

QRCodeEncoderDecoderLibrary.dll

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】なぜpublicなメンバではなくプロパティを使うのか

この記事の経緯

Effective C#4.0 の項目1「アクセス可能なデータメンバの代わりに常にプロパティを使用すること」を読んだ。
読んで得た知識をアウトプットすることで、理解度を高める。

結論

publicなメンバよりもプロパティのほうが、挙動の変更が容易にできるからだ。

なぜ挙動の変更が容易になるのか

アカウント作成を例になぜ変更が容易になるのか考えてみる。

publicなメンバを使う

まずはプロパティを使わない、以下のようなクラスを定義したとする。

Account.cs
public class Account
{
    public int Id;
    public string Password;
}
Program.cs
class Program
{
    static void Main(string[] args)
    {
        var account = new Account();
        account.Id = 1;
        account.Password = "1234567";

        var account2 = new Account();
        account2.Id = 2;
        account2.Password = "12345";
    }
}

現状、何の問題もない。
ただAccountクラスにIDとパスワードを設定するだけの処理だ。

ここで、パスワードを強化するために、パスワードの長さに制限を加えたい。
という仕様変更があったとしよう。

Account.cs
public class Account
{
    public int Id;
    public string Password;

    // 追加
    public void Validate()
    {
        if (Password.Length < 7)
        {
            throw new InvalidPasswordException("パスワードは7文字以上で設定してください。");
        }
    }

    public class InvalidPasswordException : Exception
    {
        public InvalidPasswordException() : base() { }
        public InvalidPasswordException(string message) : base(message) { }
        public InvalidPasswordException(string message, Exception inner) : base(message, inner) { }
    }
}
Program.cs
class Program
{
    static void Main(string[] args)
    {
        var account = new Account();
        account.Id = 1;
        account.Password = "1234567";
        account.Validate();  // 追加

        var account2 = new Account();
        account2.Id = 2;
        account2.Password = "12345";
        account.Validate();  // 追加
    }
}

Validateという検証用のメソッドを追加し、メソッドを呼び出すように変更を加えた。
ここで、重要なのは検証用のメソッドを利用する側が必ず呼び出さなければならないということだ。
つまり、パスワードを設定している箇所すべてにおいて、この検証メソッドを追加しなければならない。
バグ混入の可能性が大きくなることは容易に想像できる。

プロパティを使う

プロパティを使った例を見ていこう。

Account.cs
public class Account
{
    public int Id { get; set; }
    public string Password { get; set; }
}
Program.cs
class Program
{
    static void Main(string[] args)
    {
        var account = new Account();
        account.Id = 1;
        account.Password = "1234567";

        var account2 = new Account();
        account2.Id = 2;
        account2.Password = "12345";
    }
}

この時点では何も大差はない。
AccountクラスのIDとパスワードをプロパティに変更しただけだ。

ここに先程と同様の仕様変更を行う場合どうなるか。

Account.cs
public class Account
{
    public int Id { get; set; }
    private string _password;
    public string Password {
        get
        {
            return _password;
        }
        set
        {
            if(value.Length < 7)
            {
                throw new InvalidPasswordException("パスワードは7文字以上で設定してください。");
            }
            _password = value;
        }
    }

    public class InvalidPasswordException : Exception
    {
        public InvalidPasswordException() : base() { }
        public InvalidPasswordException(string message) : base(message) { }
        public InvalidPasswordException(string message, Exception inner) : base(message, inner) { }
    }
}
Program.cs
class Program
{
    static void Main(string[] args)
    {
        var account = new Account();
        account.Id = 1;
        account.Password = "1234567";

        var account2 = new Account();
        account2.Id = 2;
        account2.Password = "12345";
    }
}

プロパティのsetアクセサに対し、検証ロジックを追加した。
ここで重要なのは利用する側は一切変更していないということだ。
つまり、修正箇所がクラス内に限定されるため、バグが混入するリスクは少なくなり、挙動の変更も容易となる。

publicなメンバからプロパティに変更する際の注意点

プロパティを使ったほうが、挙動の変更が容易にできることは分かった。
それではすぐにpublicのメンバをプロパティに変更してしまおう。と思うかもしれないがそこは注意が必要だ。
なぜなら、publicなメンバへのアクセスとプロパティを介したアクセスではソース上では互換性があるかもしれないが、バイナリ上では互換性がないため、必ず再コンパイルする必要性が生じるからだ。

先程のコードが以下の構成で作られているとして、実験してみよう。
1.png

まずは、publicなメンバを持つ状態で実行してみる。

Account.cs
public class Account
{
    public int Id;
    public string Password;
}
Program.cs
class Program
{
    static void Main(string[] args)
    {
        var account = new Account();
        account.Id = 1;
        account.Password = "1234567";

        var account2 = new Account();
        account2.Id = 2;
        account2.Password = "12345";

        Console.WriteLine($"Account1 Id:{account.Id} Password:{account.Password}");
        Console.WriteLine($"Account2 Id:{account2.Id} Password:{account2.Password}");
        Console.ReadLine();
    }
}
実行結果
Account1 Id:1 Password:1234567
Account2 Id:2 Password:12345

何の問題もなく、実行できることが確認できた。
次に、publicのメンバであろうが、プロパティを使おうが利用する側のコードは変わらないため、実行ファイルはそのまま利用し、dll側をプロパティに変更して実行してみよう。

Account.cs
public class Account
{
    public int Id { get; set; }
    public string Password { get; set; }
}
実行結果
ハンドルされていない例外: System.MissingFieldException: フィールドが見つかりません: 'SampleLib.Account.Id'
...

MissingFieldExceptionが発生し、実行できなくなってしまった。
なぜこのような自体になってしまうのか、プロパティを使用した場合、実際どのようなコードを書いたことになるのか見てみよう。1

ILからのデコンパイル結果
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Security.Permissions;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class Account
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <Id>k__BackingField;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <Password>k__BackingField;

    public int Id
    {
        [CompilerGenerated]
        get
        {
            return <Id>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Id>k__BackingField = value;
        }
    }

    public string Password
    {
        [CompilerGenerated]
        get
        {
            return <Password>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            <Password>k__BackingField = value;
        }
    }
}

<○○>k__BackingFieldというprivateなメンバが自動的に生成され、それにアクセスするためのsetアクセサ、getアクセサが定義されている。
つまり、プロパティはpublicなメンバを作成するのではなく、privateなメンバとそのアクセサを提供してくれるということだ。
そのため、再コンパイルせずに実行すると、publicなメンバであるIdが存在しないという自体が生じることになる。

このような自体が起こる可能性があるため、別アセンブリから参照されているメンバをプロパティに変える際は十分に注意が必要だ。

まとめ

Effective C#4.0 の項目1「アクセス可能なデータメンバの代わりに常にプロパティを使用すること」を読んで得た知識をまとめてみた。
publicなメンバよりもプロパティを使うことのメリットが少しでも伝われば幸いだ。

それではまた。
TomoProg


  1. SharpLabを利用した。IL(Intermediate Language:中間言語)からC#へのデコンパイルがブラウザ上で簡単にできる。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

INotifyCollectionChangedのサンプルを書いてみた

今回は、タイトルの通りINotifyCollectionChangedのサンプルを書いてみました。
ただサンプルを書いてみるだけだと、理解しにくかったのでコレクションの操作処理のログを画面に表示する機能を追加してみました。


結論としては、通知だけ行いたい場合そのままのObservableCollectionをインスタンス化 1するのが一番記述が少なく済み不具合が起きづらいかなと書いてて思いました。
ただ今回みたいにコレクション操作履歴を表示したい等の処理を追加したい時はObservableCollectionを継承してInsertItem()メソッドなどをoverrideすると良い感じに拡張できるかとおもいます。
今回作成したサンプルのクラス図は以下の通りです。

class_diagram.png

いろいろ書いていますが、重要なのはViewModelはModelの影に徹しているということです。
開発リソースの配分にもよるかと思いますが、基本としてはViewModelはModelの影になっていることが望ましいと私は思っています。その方がエラー処理など煩雑な処理はModel側ですべて処理ができViewModelはその状態を受け取ってViewに通知する役割に専念でき肥大化しないからです。そして処理の見通しが良くなるかと思っています。

シーケンス図は以下の通りです。

sequency_diagram.png

処理の流れはMainWindowクラスでボタンをクリックされたらModelのメソッドを呼び出しViewModelに変更を通知します。ViewModelは受け取り、内部のプロパティなどの情報を更新し、Viewへ通知します。

本当ならMainWindowからボタンをクリックされた時は、バインドされているViewModelのコマンドが呼び出された方が望ましいと思いますが今回はINotifyCollectionChangedのサンプルということでMainWindowから直接Modelのメソッドを読んでいます。


INotifyCollectionChangedインターフェースについて

この記事をご覧になっている方には蛇足かと思いますが、一応、INotifyCollectionChangedインターフェースについて説明しておきます。

項目が追加、削除された場合やリスト全体がクリアされた場合など、動的な変更をリスナーに通知します。

msdnから引用

上記だけでは、いまいちピンとこなかったのですが、実装方法はObservableCollectionのソースコードが参考になりました。

[https://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs]

INotifryCollectionChangedインターフェース内のNotifyCollectionChangedEventHandlerイベントの第2引数のNotifyCollectionChangedEventArgsクラス内は以下のようになっています。

2019-05-02_07h51_09.png

赤枠の一番上のNotifyCollectionChangedAction列挙型が追加(Add)、削除(Remove)、置換(Replace)、移動(Move)、リセット(Reset)が通知することができます。
以下の4つは古いオブジェクトリストと新しいオブジェクトリスト、それぞれの変更されたIndexの情報を持っています。これらを用いて変更されたり追加された項目を通知することができます。

なのでINotifyCollectionChangedインターフェースを実装することで、追加、削除、置換、移動、リセット等の処理の内容と変更された項目をCollectionChangedイベントを用いて通知する機能が実装できることがわかります。


サンプルプログラム

サンプルを以下に示します。

2019-05-02_05h09_19.png

左の列がModelから伝搬されてきたリストで、2列目が操作履歴を表示するテキストボックスです。
右端は選択された項目に対して操作を行うボタンです。
作成、削除、置換、移動、リセット通知も書いてみました。

作成処理(Add)

INotifyCollectionChangedサンプル(Collection.Add通知).gif

一番最後の項目に新しい項目が追加されていることがわかるかと思います。
この処理では、SampleModelsでNotifyCollectionChangedAction.Addが呼ばれSampleViewModelsに通知されます。

削除処理(RemoveAt)

INotifyCollectionChangedサンプル(Collection.Remove通知).gif

選択されている項目が削除されていることがわかるかと思います。
削除後、操作履歴に「-1の要素がコレクションの~」という表示がありますが、これは削除後選択されている項目がないため-1がViewからViewModelに未選択という意味で通知されているためです。

置換処理(Replace)

INotifyCollectionChangedサンプル(Collection.Replace通知).gif

置換処理は以下のコードを見てもらえばわかるのですが、オブジェクトを丸ごと入れ替えています。

// 置換処理の通知を受けたSampleViewModelsクラスの処理
var ram = new Random();
int index = this._sampleViewModels.SelectedIndex;
this._sampleModels[index] = new SampleModel( "new太郎" + ram.Next(20), ram.Next(30, 70), (FoxGenusKind)ram.Next(0, 6));

上記のようにすることで、SampleModelsクラスが継承しているObservableCollectionのSetItemメソッドが呼ばれます。
SetItemメソッドが呼ばれることでOnCollectionChanged()メソッドが呼ばれ、SampleViewModelsに通知されます。
上記のようにオブジェクトを丸ごと入れ替えず、プロパティのみ変更した場合、SetItem()メソッドが呼ばれずSampleViewModelsに通知されないため、注意してください。
一部のプロパティの変更をViewModelsに通知したい場合は、各項目のModelのPropertyChangedイベントをViewModelから受け取ってその変更をViewModelsへPropertyChangedで通知する必要があります。(今回のサンプルはINotifyCollectionChangedのみの対応です。)
以下のような流れになります。

IPropertyChangedSample.png

移動処理(Move)

INotifyCollectionChangedサンプル(Collection.Move通知).gif

見づらいですが、選択されている項目が上下に移動していることがわかるかと思います。

リセット処理(Clear)

INotifyCollectionChangedサンプル(Collection.Reset通知).gif

リセット処理後、操作対象のリストがすべてなくなっていることがわかるかと思います。

サンプルプログラムは以下のURLに置いていますので、興味のある方はご自由にご覧下さい。

[https://github.com/Pregum/hatena_blog/tree/master/ObservableCollectionImplementSample]


ObservableCollectionのプロパティ通知方法について

今回はModelのコレクションクラスはObservableCollectionクラスを継承していますが、ObservableCollectionクラスのPropertyChangedイベントはprotectedで記述されているため、SampleViewModelsでSampleModelsの通知を受け取るためにはキャストする必要があります。

2019-05-02_06h55_17.png

今回はSampleModelsから操作履歴と選択されている項目のIndexの変更通知を受け取るために使用しています。

// SampleViewModelsのコンストラクタ内の処理
((INotifyPropertyChanged)this._models).PropertyChanged += this.SampleViewModels_PropertyChanged;

ObservableCollectionから呼び出したメソッドと発火されるCollectionChangedイベントのアクションの種類の対応表

ObservableCollectionクラスは外部のクラスから直接CollectionChangedイベントを発火させることはできませんが、Add, Remove, Replace, Move, Resetに対応するメソッドは存在していますので、その対応表を以下に示します。

ObservableCollectionクラスの呼び出されたメソッドと発火されるCollectionChangedイベントのアクションの種類の対応表

呼び出すメソッド 内部で呼ばれるメソッド 発火されるCollectionChangedイベントのアクションの種類
Add() InsertItem() Add
Insert() ※今回は使用していません。 InsertItem() Add
RemoveAt() RemoveItem() Remove
Remove() ※今回は使用していません。 RemoveItem() Remove
this[index] SetItem() Replace
Move() MoveItem() Move
Clear() ClearItems() Reset

※ Move()メソッドはObserableCollection.csに記述されていますが、Add()メソッド等はCollection<T>クラスで実装されているメソッドなので、Collection<T>クラスを見ないとわからないです。

また、CollectionChangedイベントのみ着目していますが、一緒にコレクションの個数のプロパティ(Count)やインデックスを表すプロパティ(Item[])も変更されていますが、ObservableCollectionを使用しているのであれば特に気にしなくて良いと思います。サンプルプログラムでもCountやItem[]の変更通知は受け取っても何も処理を行っていません。

補足

一度理解すれば、PropertyChangedイベントの実装と同じような感じで理解できるかと思います。
私自身、コレクションの更新イベントが理解できていなかったので今回サンプルを書いたことによって少し理解ができて良かったです。
サンプルプログラムでは、各単体のオブジェクトの通知は実装していないため、実際に使用する際には実装する必要があるかと思いますが、そちらはたくさん他の方のサンプルがあると思いますのでそちらをご覧ください。
また今回は単体の変更通知のみなので実装していませんが、AddRange()等で複数の変更通知を行いたい場合は通知を受け取った時foreach等でループを回して処理を行う必要があります。
少しでも読まれた方の助けになれば幸いです。
読んで頂き、ありがとうございました。


参考サイト


  1. ジェネリッククラスをObservableCollection<SampleModel>等具象型にすること。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む