Vue.js とやらを勉強2
Vue.js で基本の属性をまとめる。
目次
ほとんど使わないじゃないかなって思ってるやつ。
v-html="---html---" で指定した html が差し込めるってやつ。 サンプル。
ボタン押すと入力した文字がどんどん追加されるってやつ。
こういう、追加作業するときって Web アプリだと多いのだろうか・・・。
そんななさそうなので、「使わない」と勝手に思っている。 html 中に if 文が書き込めるやーつ。
ちゃんと else も書ける。 サンプルはボタン押すと内容が切り替わるよってやつ。
まとめてないけど template 構文を使ってる。 template 構文のおかげで、中身をまるっと変更できる。
後述するコンポーネントとか、実際に Vue.js でアプリ
作ってくってなるとこの template 構文が基本になる(と思っている)。 v-if = "条件" といったふうに書く。
「条件」に bool 値の変数名を書いて、data がその bool 値持ってれば
Vue.js がきれいに変数として扱ってくれる。ありがたい。 さっきの v-if と一緒で html に for 文が書ける。
これのおかげでリスト表示とかもらくらくってわけ。 注意すべきなのは、v-if と併用すると
「v-if が後になる」ってことくらい?
v-for が優先で動く。 使い方は v-for = " val in values " みたいなかんじ。
values がオブジェクトだとプロパティも抜き出せて、
( obj, prop ) in values みたいな書き方になる。 index 番号が欲しい時もオブジェクトと同じ書き方で、
( element, index ) in values になる。
( javascript の for-in ループとは違うのがちょっと面倒・・?) サンプルはオブジェクトを対象に、さらに v-if を組み合わせたやつ。 すーぱー使う。絶対に使う。
必ず習得すべきもの。 できることは、新しい html のタグを作るようなもの。
C# でいうところのカスタムコントロールとかユーザコントロール。
ユーザコントロールのが近いかな。 このコンポーネントにプロパティ持たせて、そこに値をバインドとかできる。
XAML みたいだと思えば楽かも。(こういう時、自分の得意な言語にあてはめて考えれるのは楽だ) 単一のファイルでコンポーネントを作るときは Vue の関数を呼び出す。
そうでない場合、つまりちゃんとしたアプリだと *.vue っていう拡張子が vue の
ファイルに書く感じ。 今回は前者で書く。 Vue.component(conponentName, { conponent が持ついろんなプロパティ }) こんな感じで使う。
conponentName はコンポーネント名で、これがタグになる。文字列で指定する。 第2引数はコンポーネントの本体で、オブジェクトで指定する。 ほんじゃまかサンプル。
「Hello ○○」って書いてくれるだけのコンポーネント。
サンプルは以下の仕様になってる。 v-bind については後述する。 v-for で複数の name を取得する。names は Vue オブジェクトが持つ data に設定されたプロパティである。 1つ取り出した name が v-bind によってコンポーネントの name 属性にバインドされる。 そして、name 属性が template の {{ name }} 部分に入る仕組み。
name 属性は props: ['name'] で設定されている。(配列なんで複数書けるよ!) Hello 部分を担う message はコンポーネント内の data プロパティが返している message プロパティのこと。
こいつが {{ message }} に入る。 間違いなく超使うやつ。
バインドなんで、XAML でもいっぱい出てきてる。
つまり、特定の値に特定の値を割り当てる感じ。
ただし、v-bind は単方向だから、入力値をプロパティに代入とかはできない。
双方向でバインドしたいときは v-model を使う。 ちょっと前に v-bind で name 属性に変数 name をバインドした。
つまり、こいつは属性に対してバインドできる。 つまり、自分で設定した属性だけでなく html のタグの style とか class にもちゃんとバインドできる。 ほんじゃサンプル。
ボタンを押すと文字色が変わる。
文字色は css 側で class 「red」, 「blue」に対して変えてる単純なもの。 classes でクラスを代入してる。プロパティ名がそのままクラス名になる。 そんで、bool 値に従ってクラスが適用される。 サンプルだとプロパティでクラスを代入してるが、下記のようにばちこり書いてもOK。 class="{red: isRed, blue: isBlue}" v-model もバインドなんだけど、 違った、v-model は双方向バインドのこと。
XAML でもあったやつ。入力した値をプロパティに渡すことができるし、プロパティの値を表示したりもできる。 これ以下の説明は一応そのとおりなんでそのままにしておくが、双方向バインドなんでもっとうまいことできるし、
ちゃんと考えないとたぶんだが沼る。 v-bind と組み合わせれば、input の値をプロパティにバインドして、プロパティにバインドした値を
属性にバインドする。これでリアルタイムに input の値がほかのタグに反映される。 下記がサンプル。
上記で書いてるとおり、input の内容を hello コンポーネントに渡している。 v-model でまとめたとおりの簡単なサンプルなので、これ以上は特に説明なし。 v-on はイベントの発生のこと。
指定した関数を実行してくれる。 下記のように書けば、click 時に func1 が実行される。 v-on:click="func1();" 複数個の関数を実行したいときは v-on:click="func1();func2();func3()" みたいに複数個繋げて書く。 コンポーネントに関数をオブジェクトとして持てる。 methods というプロパティを用意し、そこにオブジェクトを書く。
プロパティ名が関数名になり、「function () { ... } 」で関数を設定してやればいい。 サンプルはボタンを押すと以下の機能が働くもの。 文字色はクラスを変更し、 css で変えてる。
カウンターとかメッセージは関数の中身でゴリゴリと。 v-on も好きな関数を実行できるんで、よく使うだろうなぁ。 次はばちこり vue/cli でアプリ作ったやつをまとめたい。
まだ勉強中なんでまたまとまってきたら書く・・・。v-html
<div id="app">
<ol v-html = "message"></ol>
<hr>
<input type="text" oninput="tempValue(event);">
<button onclick="add();">add</button>
</div>
<script>
let data = {
message: '<li>one</li>',
tempInput: ''
}
let app = new Vue({
el: '#app',
data: data
});
function add(){
data.message += `<li>${data.tempInput}</li>`
}
function tempValue(event){
data.tempInput = event.target.value;
}
v-if
<div id="app">
<template v-if="flag" >
<p>Display data table.</p>
<table>
<tr><th>Name</th><th>mail</th></tr>
<tr><td>Taro</td><td>taro@yamada</td></tr>
<tr><td>Hanako</td><td>hanako@flower</td></tr>
<tr><td>Sachiko</td><td>sachiko@happy</td></tr>
</table>
</template>
<template v-else>
<p>Display data list</p>
<ul>
<li>name: Taro, mail: taro@yamada</li>
<li>name: Hanako, mail: hanako@flower</li>
<li>name: Sachiko, mail: sachiko@happy</li>
</ul>
</template>
</div>
<button onclick="onChangeFlag();">
change flag
</button>
<script>
let data = {
flag: true
}
let app = new Vue({
el: '#app',
data: data
});
function onChangeFlag(){
data.flag = !data.flag;
}
</script>
v-for
<div id="app">
<p>Display data list</p>
<table>
<tr>
<th>name</th>
<th>mail</th>
<th>tel</th>
</tr>
<!-- v-for が優先で動く。v-if は v-for のあと。 -->
<tr v-for="( obj, key ) in items" v-if="obj.isPublic">
<td>{{ key }}</td>
<td>{{ obj.mail }}</td>
<td>{{ obj.tel }}</td>
</tr>
<tr v-else>
<td>{{ key }}</td>
<td>*** 非公開 ***</td>
<td>*** 非公開 ***</td>
</tr>
</table>
</div>
<script>
let data = {
items: {
Taro: {mail: 'taro@ssss', tel: '777-777', isPublic: true},
Machiko: {mail: 'machiko@ieiieieie', tel: '888-888', isPublic: true},
Administrator: {mail: 'admin@opertor', tel: '999-999', isPublic: false},
Tester: {mail: 'tester@test', tel: '999-999', isPublic: false},
Takeshi: {mail: 'takeshi@iwatype', tel: '999-999', isPublic: true}
}
};
let app = new Vue({
el: '#app',
data: data
});
</script>
コンポーネント
<div id="app">
<hello v-for="name in names" v-bind:name="name"></hello>
</div>
<script>
Vue.component("hello", {
props: ['name'],
data: () => {
return {
message: "Hello ",
};
},
template: '<p class="hello">{{ message }} {{ name }}</p>',
});
let app = new Vue({
el: "#app",
data: {
names: ['taro', 'hanako', 'sachiko', 'takeshi']
}
});
</script>
v-bind
.red {
color: red;
}
.blue {
color: blue;
}
<div id="app">
<p v-bind:class="classes">{{ message }}</p>
<button onclick="onChnageColor();">{{ btnText }}</button>
</div>
<script>
let classObj = {
// プロパティ名がそのままクラス名になる
red: true,
blue: false
};
let data = {
message: "This is Message.",
classes: classObj,
btnText: "To Blue"
};
let app = new Vue({
el: "#app",
data: data
});
function onChnageColor() {
classObj.red = !classObj.red;
classObj.blue = !classObj.blue;
data.btnText = classObj.red ? "To Blue" : "To Red";
}
</script>
v-model
こいつは input タグの値をプロパティにバインドするやつ。 <div id="app">
<!-- name 属性に name プロパティを代入してる -->
<hello v-bind:name="name"></hello>
<!-- v-model により name プロパティに対して値が代入される。 -->
<input type="text" v-model="name">
</div>
<script>
// name プロパティを定義し、html 側で name 属性を指定すればここのプロパティとして入る
Vue.component('hello', {
props: ['name'],
template: '<p class="hello">Hello, {{ name }}</p>'
});
let app = new Vue({
el: '#app',
data: {
name: 'no-name'
}
})
</script>
v-on
<div id="app">
<click></click>
</div>
<script>
Vue.component("click", {
methods: {
// この this は component 内のネストのみ
onChangeColor: function () {
this.classes.red = !this.classes.red;
this.classes.blue = !this.classes.blue;
},
onAddCounter: function () {
this.counter++;
},
onChangeMessage: function () {
this.message = this.classes.red
? `To Blue counter: ${this.counter}`
: `To Red counter: ${this.counter}`;
},
},
data: function () {
return {
counter: 0,
classes: {
red: true,
blue: false,
},
message: `To Blue counter: 0`
};
},
template:
'<button v-on:click="onChangeColor();onAddCounter();onChangeMessage();" v-bind:class="classes">{{ message }}</button>',
});
let app = new Vue({
el: "#app",
});
</script>
次にまとめるの
Vue.js とやらを勉強
ありがたいことにこのご時世でも次の働き先が見つかった。 そういうことで転職の関係で次に使うやーつを勉強中(XAMLじゃないんかーい)。 ということでこれまでやってきたやつを複数回に分けてまとめる。 仕事もあって input も大量にしなきゃでまとめる時間がない。
目次
こいつはいっぱいある javascript とかで Web アプリ作るためのフレームワーク。 Nuxt.js っていうルーティングのためのフレームワークと組み合わせるけど、まだ
Nuxt.js は勉強してないんであとで。 ルーティングは複数ページ扱う Web アプリでページの遷移とかぐちゃぐちゃできる。
Node.js と Express っていうフレームワークとおんなじ。 割と普通のスクリプト言語なんだけど、Web の物体を動かせる(らしい)。
実はまだそこの動かすとこまでやれてない。 ただ、HTML + CSS + JavaScript はもうセットっぽい。
そこのフレームワークとして React とか Node.js とか Vue.js とかいろんなのが乱立してる。
戦国時代みたい。 スクリプト言語として勉強した印象は、this とか var / let とか巻き返し(だっけ?)とか
面倒なこともあるけど、まぁ普通のスクリプト言語って感じだった。 Vue.js の使い方もほんといっぱいあるけれど、まずは単一ファイルで使う基本から。 HTML ファイルを用意して、下記を head タグに入れる。 単一ならこれで一応使える。
body 部分は以下のようになる。 script タグ部分は javascript で書いて、message への値反映とかを vue.js がやってくれる感じ。 el プロパティは id 「app」に対して渡さなきゃいけないっぽい。
( id に「app2」とか用意して複数個やろうとして失敗はした。) ほんで、data 部分はオブジェクトで書く。今回みたいに HTML 部分に渡したいなら
同じプロパティで用意すること。 data にはオブジェクトを渡せばいいので、関数もいける。 関数なんでアロー関数も書ける。
かなり入力については寛容なようす。 ボタンを押してさっきの message を切り替える。 onclick 内に実行させたい関数名を書く。
そんで、script 部分にその関数を書くってわけ。 さくさく行く。次はテキストの内容をそのまんま反映する。
XAML とかでいうところの TextChange イベント。 oninput に実行したい関数を引数付きで書く。
event 引数には input タグが紐づいていて、その値を
「event.target.value」で抜き出してる。 v-bind とか v-html, v-if, v-for など HTML の属性として扱う重要なやつを
単一ファイルのものでまとめる。 それらをまとめ終わったら、vue/cli も使っていった
vue アプリについてまとめる。 この属性は基本中の基本で大事だし、v-for は知らないと
繰り返しでタグを用意するのがめちゃめちゃに面倒になる。Vue.jsって
JavaScript って
単一ページで使う Vue.js
<script src="https://unpkg.com/vue"></script>
<body>
<!-- message 部分を Vue.js で渡す -->
<div id="app"> {{ message }} </div>
<!-- vue.js 部分。 javascript で書く。 -->
<script>
let app = new Vue({
el: "#app",
data: {
message: "Hello World!"
}
})
</script>
</body>
<script>
let app = new Vue({
el: "#app",
data: function() {
return {
message: "Hello World!"
}
}
})
</script>
ボタン押して message 部分書き換え
<body>
<div id="app">{{ message }}</div>
<hr>
<button onclick="onClick();">click</button>
<script>
let data = {
message: 'Hello World',
}
let app = new Vue({
el: '#app',
data: data,
});
function onClick() {
data.message = 'You Clicked';
}
</script>
</body>
テキストの内容を反映
<body>
<div id="app">{{ message }}</div>
<hr>
<input type="text" oninput="textInput(event);"/>
<script>
let data = {
message: 'Hello World',
}
let app = new Vue({
el: '#app',
data: data,
});
function textInput(event) {
data.message = event.target.value;
}
</script>
</body>
次まとめること
Windows 11: WSL2 による Ubuntu 環境構築
タイトルのとおり、Winsows 11 で WSL2 の Ubuntu 環境を構築していく。
最初は前回記事みたいに仮想環境でやってたんだけど、「あれ、WSLで全部いいんじゃね?」ってなってしまって、 結局こっちにした。
目次
今回は Windows 11 前提で書く。
WIndows 11 はまだプレビュー段階で、Dev Channel に入らないと
Windows のアップデートすらできないはず。 なんで Windows 11 の環境でやるかって、この OS のバージョンから
WSL の環境構築がむちゃんこ簡単になるから。 ではやっていく。 まずは Windows Powershell を開く。 下記コマンドを打つ。 wsl install -d Ubuntu-20.04 おわり!!
あとは待つだけ。 いやー、簡単すぎる・・・。
ちなみに、インストールできるディストリビューション( WSL でインストールする OS )は
下記のコマンドで見れる。 wsl --list --online もひとつ、Ubuntuのインストールが終わるとユーザ名とパスワードを設定してくれと言われるのでちゃんと設定する。 注意点として、このインストールは WSL だとできない。 WSL 2 にアップデートする必要がある。
ということで下記にちょろっとまとめておく。 GUIのインストーラでサクッとできるので紹介。
安心と信頼の Microsoft 公式ドキュメント。 ここの「最新のパッケージをダウンロードします。」からインストーラをダウンロードして実行するだけ。
とっても簡単。 ただ、上記のドキュメントでも紹介しているとおり、WSL 2 を既定のバージョンにするのを忘れずにやっておく。 下記コマンドを実行する。 wsl --set-default-version 2はじめに
WSL の Ubuntu 環境構築
WSL 2 への更新
NATを使って仮想環境(Ubuntu)構築
目次
NAT(Network Address Translation)を使って仮想環境を構築したんでまとめる。
仮想環境が欲しくなった理由は、Ruby on Rails を勉強しようとして Unix 環境ほしくなったkら。 自身のPC内に閉じたネットワークを作って、その中に仮想環境を置くイメージ。 通信時はホスト側のIPになって、NATがうまいことゲスト側に渡してくれる(自分なりの理解)。 これは環境によると思う。自分はNATを使わんといかんかった・・・と思う(他の方法もあったかも)。 とりあえず理由はたった一つ。 HWの都合上、外部スイッチはアダプタを共有しないといけなくて、そうするとゲストとホストのIPがかぶる。だからホストからゲストに通信するってことができない。 ってこと。 だもんで、NATで ipv4 のアドレス作って内部で通信して、かつその ipv4 で github にも接続できるようにしたろって。 以下はできるようにしてく。 やることはざっくり以下の3つ! Ubuntu をインストールする。ここは割愛。 Hyper-V のクイック作成だかなんだかですぐにできる。 かなーり苦戦した。 手順は以下のとおり。 管理者権限で powershell を起動する。(別にHyper-VのGUI使ってもよし) 以下のコマンドを実行。 new-vmswitch -name XXX -switchtype Internal XXXのところは自由。 これにはまず ifIndex (InterfaceIndex)が必要なので、下記コマンドで確認する。 get-netadapter 新しく作った内部スイッチの ifIndex を確認すること。11 とか、16 の数字部分だけでよい。 その後、下記コマンドで ifIndex のネットワークアダプタに対してIPとサブネットマスクを設定する。
今回は例なのでとりあえず ifIndex は 11 にする。 new-netipaddress -IPAddress 192.168.1.254 -PrefixLength 24 -InterfaceIndex 11 IPアドレスはここでは適当。サブネットマスクは「255.255.255.0」になる 24 。
これで、IPが 254個作れるのかな?(ネットワーク弱者) 個人用だったらぶっちゃけこんないらない。 下記のコマンドでNATネットワークを作る。 new-netnat -Name YYY -InternalIPInterfaceAddressPrefix 192.168.1.0/24 YYY のとこは自由。 InternalIPInterfaceAddressPrefix はちゃんと前述した IP アドレスを含むようにする。 ※ このNATネットワークは1個までしか作れない こっからhyper-vで。 これまでの設定をした内部スイッチを仮想環境のネットワークアダプタに割り当てる。 ここの詳細手順は省略。 仮想環境を起動し、仮想環境内で IP アドレスを設定して固定する。 サブネットマスクは 「255.255.255.0」なんで、
「192.168.1.0」から「192.168.1.253」までかな?(ゲートウェイあるんで減るはず?) 例えば、以下のように設定する。 サブネットマスクとデフォルトゲートウェイは「作った内部スイッチ次第」なんで注意。 以上で NAT の導入は終わり。
ゲストからホストに繋げるし、ホストは外部につなげるはず。 念のため NAT が機能しているかどうかを下記コマンドで確認する。 get-netnatsession これで何か表示されたらOK。 何も表示されないならなんかおかしい。
再起動かけるとかいろいろする。サブネットマスクとかはちゃんと確認する。 これは Windows から Ubuntu にリモートデスクトップ接続するために必要。
なんか通信方式が違うらしく、初期状態ではリモートデスクトップではできない。 Ubuntuからターミナルを開き、下記を実行する。 sudo apt install xrdp これで、インストールだけは完了 ufw でポートの「3389」を許可する。 下記コマンドを入力する。 確認する。下記コマンドを入力する。 sudo ufw status すると以下の結果が返ってくるはず。 Status: inactive とか表示されるときは、ufw がそもそも動いてないので、
下記コマンドで起動する。 sudo ufw enable NAT構成だからかなんか知らないが、なんか xrdp の起動にすら失敗するので
xrdp.ini を編集する。 エディタはなんでもよいが、デフォの gedit 使う。 sudo gedit /etc/xrdp/xrdp.ini ファイルが開けたら、「port」の設定してるとこ探して編集する。 「ipv4 address」には設定した ipv4 のアドレスを入れる。これ入れないとなんか xrdp 起動しても接続できなかった。 これで起動できるはず。下記コマンドで xrdp を起動する。 sudo systemctr start xrdp Ubuntu の設定から「Sharing」を開き、画面だけ共有するよう設定する。 共有を Active にするために、画面右上にある toggle ボタンで編集可能なよう切り替える(わかりにくいわ!!)。 切り替えたら「Screen Sharing」を下記のように設定する。
いろいろアクセスするにはパスワードを要求するようにしている。 これでリモートデスクトップで接続するための設定が完了する。 Ubuntuからログアウトし、ホスト(Windows)からリモートデスクトップで「 Ubuntu に設定した ipv4 の IP アドレス」に接続する。 ログアウトは xrdp において必須らしい。 xrdp の設定でお世話になった先駆者様。 最後に、ipv4 だけだと死ぬほど遅かったので ipv6 の仮想スイッチを作り、仮想環境に割り当てる。 これでネットワークを十分に高速にしたうえで、ipv4 の環境が完成する(はず)。 ただ、おそらく ipv4 が優先されるので、ipv6 を優先してほしい場合は以下の手順で ipv6 で接続するようにする。 下記コマンドでサクッと確認できる。
なお、ipv6 がなければ ipv4 でやるしかない・・・。 host [ドメイン] 調べた ip を登録する。下記コマンドでエディタ開いて編集する。 sudo gedit /etc/hosts ここで、「ip\tドメイン」とか「ip\sドメイン」の書式で追加するだけ。 以上で、全部終わり!!NATを使って仮想環境構築
なんでWindowsじゃないかって、Rubyでキャリッジリターンあるだけでデプロイ失敗して、「うがあああ」ってなったから。
ちなみに、NATって仕組みを使うとグローバルIPアドレスを節約できる。
普通だったら外部の仮想スイッチ作って終わりだが、NATでやらないといけなかった理由がある。NAT を使う理由
別個の ipv4 アドレスが欲しかった
これだけ。こうなった理由が下の二つ。
これの何がいかんて、
ただの外部スイッチで ipv6 設定だと github に繋げなくて開発できない仮想環境でできてほしいこと
仮想環境の構築
Ubuntu のインストール
NAT の導入
内部スイッチつくる
IPとサブネットマスクを設定する
NATネットワークを作成する
内部スイッチを仮想環境に割り当てる
仮想環境でIPアドレスを設定する
項目
内容
IPアドレス
192.168.1.100
サブネットマスク
255.255.255.0
デフォルトゲートウェイ
192.168.1.254
DNS
8.8.8.8
確認
xrdp の導入
firewall の設定変更
xrdp.ini の編集
画面の共有設定の変更
確認
引用
最後に高速接続のために
指定ドメインの ipv6 確認
/etc/hosts の編集
C#:自作属性を使ってコマンドラインオプション処理を1クラスに
目次
C#の属性を使った処理をまとめる。(転職活動が忙しすぎて更新できんかった・・・)
最初は一般的なことを書くが、後半は
属性を自作して処理を追加するといったことまでまとめる。 属性にできることはいっぱいある。一例を書く。 割と使うことなるのは下記あたりか。 最後のは Form アプリケーションやってる人だったらよく見るやつ。 結局ここら辺の属性ってのは、「フィールド・プロパティ・メソッドがなんたるか」、というのを
定義してくれている。 シリアライズ対象だったり、古いやつだったり、シングルスレッドで動かしたりといった具合だ。 作り方をまとめる。作るうえで例がないとわかりにくいので、
今回はコマンドラインオプション処理を任せる属性を作る。
( python ならライブラリあるじゃんww というのはNG。C#が使いたいんだよ!) コマンドラインオプションでやってほしいのは以下のとおりにする。
全力で作りこむならもっと機能が必要になる。 先にどんなものを作るのか結果を載せておく。下記のように、フィールドやプロパティに対して
属性を指定することで、コマンドラインオプションと入力された値の格納先を指定する。
そして、コマンドラインオプションのチェックだとかなんかはぜーんぶ1クラスに任せてしまうというもの。 コード。 使ってみるとこんな感じ
今回試す環境は下記のとおり。やっとこさ .NET 5 でやるけどもうすぐ .NET 6 くるとかいう絶望。 まず作るのはカスタム属性クラスである。Attribute クラスを継承したクラスである。 作ったクラスのコンストラクタを考える。
こいつが重要なのだ。なぜなら、コンストラクタの引数は属性に渡すパラメータそのものだからである。 今回は3つ必要になる。 まずはコマンドラインオプション側から。 続いてコマンドラインターゲットクラス。 Inheritedプロパティはあとあと使うので作った。まぁ、正直いらないような気もしてる。 残りの機能を作っていこう。あとは以下の3機能だ。 さて、さっき作ったのはただの属性だ。
今回作る機能は Parser クラスを作って実行されることにする。 このクラスにどんどん機能を盛り込んでいく。まず、「必須オプションの有無を確認する機能」から。 この機能にまず必要なのは以下の2点。 1はだいぶ簡単に取得できる。
下記で終わりだ。 .NET 5 なので配列の範囲指定を使用している。たしかC# 8.0 以降の機能なんで、
.NET Framework 4.8 の場合は適宜変更が必要(フレームワークによってデフォのC#バージョンが決まるため)。 問題は2と3である。
これはまず細分化する。 である。 まずは全部のフィールド・プロパティを取得するものから。
とはいっても、できるだけ最小限にしたいので「CommandLineAttr」属性が
引っ付いてるもののみにする。 「_target」は「CommandLineTargetClass」属性がついているクラスのTypeインスタンスである。
「GetRuntimeFields」と「GetRuntimeProperties」でフィールドとプロパティを全部取得する。 private だの public だの static だのは関係なくすべて取得できる。 そして、次の foreach で「CommandLineAttr」属性を持つものだけを返す。
「GetCustomAttribute」メソッドで「CommandLineAttr」属性が抜き出せるかどうかチェックしている。
抜き出せない場合は null が返ってくる。抜き出せたら、「CommandLineAttr」属性を持っているということ。 次に「CommandLineAttr」属性のインスタンスを取得する。
さっきのメソッド使って foreach し、「GetCustomAttribute」メソッドで取得していく。 さっきと似たような「GetCustomAttribute」の書き方をしてわざわざ別メソッドにしてるのは、
「getAllCommandLineMember」メソッドは「コマンドラインオプションで指定された値をフィールドやプロパティに代入する」際にも
使いたいからである。 最後に、Required プロパティを確認しながらチェックしていく。 最後の「chkRequired」メソッドが今回の目的のメソッドである。
foreach でループを回す際に、「Required」プロパティが true の「CommandLineAttr」属性に絞り込み、
さらにそれをユーザが指定したコマンドライン引数と同じ形になるよう Select してやる。 Selectしたやつが Args というコマンドライン引数に「存在しない」なら、
必須オプションが指定されていないということである。
その時はエラーメッセージを設定し、false を返す。
すべての必須オプションが存在すれば true を返す。 次に、各フィールドやプロパティにコマンドライン引数で指定された値を格納する機能を作る。
まずは細分化、もとい簡単なアルゴリズムを。 1は既にメソッドとして作った。
2は「GetCustomAttribute」メソッドから「CommandLineAttr」属性のインスタンスを取得し、作る。 なので、ここまでは簡単。 次からがちょっと厄介だが、今回は4を実現するために辞書を作ることにした。 Keyには入力されたコマンドライン引数のオプション(ex. -i , -o など)、 Valueにはその時指定された内容(ex. -i input.txt の input.txt 側)を格納する。 これで、TryGetValue メソッドを使っていろいろできるようになる。 以下がそのコードである。 今回、辞書を作る過程で「重複したオプションがあると例外が発生する」ようになっている。
もしここでそんなことが起きてほしくない場合は、別途メソッドを追加する必要がある。 また、値をキャストする「tryGetOptionValue」メソッドはキャスト対応が間に合っていない。
別に List 型だってやろうと思えばできるのである。しかし、今回は最低限として、 とした。 memType はフィールド・プロパティのタイプのことである。このタイプに沿ってキャストする。
見てみるとわかるが、int や double は変換に失敗すると例外がでる。 今までの2機能を CommandLineParser クラスのコンストラクタで呼び出す。
これにより、 CommandLineParser クラスのインスタンスを作った時点で
「必須オプションのチェック」、「フィールド・プロパティに対する引数値の代入」が
完了することになる。 コンストラクタの引数は「CommandLineTargetClass」属性を付与したクラスの Type インスタンスである。
「CommandLineTargetClass」属性がついてるかどうかのチェックメソッドも追加した。 最後の機能はヘルプメッセージを出力する機能である。 「-h」とか指定すると表示されるやつ。 「CommandLineAttr」属性の引数にヘルプメッセージ用の string 型がいるのは、
ヘルプメッセージのフォーマットを固定にして、そのオプションの説明を指定できるようにしたかったから。 そしてこれをプロパティで取得できるようにすることで、デバッグ時にコピペできるようにもする。 出力する情報は以下のとおり。 メッセージは StringBuilder を使って作る。 いろんなツールでこの機能が使えないと不便なので、
アセンブリから会社名とかを抜き出し、 StringBuilder に格納する。
これにより、適宜アセンブリを設定するだけでヘルプメッセージが変わってくれるようになる。
これでツール説明部分が完了である。 次に、これまでに作った「CommandLineAttrList」プロパティを使って「CommandLineAttr」属性を全部抜き出す。
そこから、必須属性だけを抜き出し、それをメッセージ用の StringBuilder に追加してやる。
あとはサクッと「[ options ... ]」を追加してやって usage 部分は完了。 次に、各オプションのヘルプメッセージ部分。
「CommandLineAttrList」プロパティで全部抜き出してるんで、それで foreach を回し、ヘルプメッセージ参照するだけである。 今回は長さでソートしてみた。本当はアルファベット順とか、必須のやつは先頭とか別のソートのがいいのかもしれない。
必須のオプションとそうでないオプションをグループ分けして表示するのもよさそうである。
ここはすごい改良の余地が残る。 最後にヘルプメッセージ表示のためのオプションを足してヘルプメッセージは完了である。 とっても簡単だが、指定オプションがヘルプかどうかのプロパティも載せておく。 全体コードも載せる。git にも上げてるけど、ブログはブログで完結しててほしいんで。 属性でできることや、自作属性の作り方、また自作属性を使ったコマンドラインオプションをまとめた。
属性を使ってできることはもっともっとあるだろうし、理解しておくとすごいできることが広がる(と思うのでまとめて考えを整理)。 ただ注意したいのは、今回作った CommandLineParser クラスは肝心要の「引数で指定した値のチェック」や
「重複オプションのチェック」などはしていない。それらは適宜追加が必要である。
ただ、これも列挙型を作り「このコマンドラインオプションに来る値の種類」を指定できるようにすると、
「引数で指定した値のチェック」もできるようになる。 例えば、ファイルの存在有無とか。 また、最近では「runコマンドに対するヘルプ」といった単位でヘルプを参照できる。
「docker run -h」などで「run のヘルプ」が見れるといった形である。
こういったモダンな感じにするのも忘れてはいけないと思う。 以上で属性のまとめ。
結構長くなってしまった。
自分も使っていこうかなぁと考えながら作ったので、このコマンドラインオプションを認識するコードはgitから見れるという。 ダウンロードもしやすいようにリポジトリ作った。これでブランチも作りやすいね。
属性でできること
3つ目が、とってもいいことだと思う。
コード内で完結してくれれば無駄に管理物が増えないからだ。
また、コードはGitやSVNなど構成管理をするので、勝手に何がどう変わったかをチェックできる。
属性を使う
自作属性を作る
[CommandLineTargetClass]
class Program
{
[CommandLineAttr("i", true, "help: input file")]
public static string InputFile { get; set; }
[CommandLineAttr("o", false, "help: output file")]
public static string OutputFile { get; set; } = @"C:\temp\outputFile.txt";
[CommandLineAttr("test", false, "help: test option")]
static string privateTest { get; } = "Test!!";
[CommandLineAttr("mode", false, "help: mode")]
static int _mode = 0;
[CommandLineAttr("debug", false, "help: debug mode start")]
static bool _debug = false;
static void Main()
{
var parser = new CommandLineParser(typeof(Program));
if (parser.IsHelp)
{
Console.WriteLine(parser.HelpMessage);
return;
}
if (parser.IsError)
{
Console.WriteLine(parser.ErrorMessage);
return;
}
// main process start ...
Console.WriteLine($"default: none\t\t\t\t\tvalue: {InputFile}");
Console.WriteLine($"default: C:\\temp\\outputFile.txt\t\tvalue: {OutputFile}");
Console.WriteLine($"default: Test!!\t\t\t\tvalue: {privateTest}");
Console.WriteLine($"default: 0\t\t\t\t\tvalue: {_mode}");
Console.WriteLine($"default: false\t\t\t\tvalue: {_debug}");
}
}
環境
クラスを作る
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineAttr : Attribute
{
}
完了だ。
実はもうこれだけで自作属性はできた。もう使える。
次に行く前にいくつか説明を行う。
項目
説明
AttributeUsage
属性を付ける対象を選べる。今回はコマンドラインオプションなので、フィールドとプロパティにした。
AllowMultiple はこの属性を複数つけれるようにするかどうか。今回は1つだけにしたいんで false 。
Inherited は属性を継承させるか。これは「この属性を付けているプロパティなどに対して」という意味。
sealed
継承不可にする便利なやつ
Attribute
継承元クラス。これを継承するだけでなんと自作属性完成だ。
さっき作ったのが「プロパティやフィールドをコマンドラインオプションとして認識するための属性」である。
あともう一個、「そのクラスがコマンドラインオプションを持つクラスかどうか確認するための属性」を作る。
とはいっても作り方はすでに説明したとおり簡単。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineTargetClass: Attribute
{
}
コンストラクタを考える
/// <summary>
/// deal Command line option Attribute
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineAttr : Attribute
{
static bool? _inherited;
/// <summary>
/// CLOption Name
/// </summary>
public string CLOptionName { get; }
/// <summary>
/// Is Option Required
/// </summary>
public bool Required { get; }
/// <summary>
/// Help Message
/// </summary>
public string HelpMessage { get; }
/// <summary>
/// Is Inherited Property of AttributeUsage
/// </summary>
public static bool Inherited
{
get
{
if (_inherited != null) return _inherited.Value;
var attr = typeof(CommandLineAttr).GetCustomAttributes(typeof(AttributeUsageAttribute), true)[0] as AttributeUsageAttribute;
_inherited = attr.Inherited;
return _inherited.Value;
}
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="clOptionName">Command line option name</param>
/// <param name="required"> Is option required </param>
/// <param name="helpMessage">Help message</param>
public CommandLineAttr(string clOptionName, bool required, string helpMessage = null)
{
CLOptionName = clOptionName;
Required = required;
HelpMessage = helpMessage;
}
}
/// <summary>
/// this attribute is used by developers to check Command Line Attributes for properties or fields in Class..
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class CommandLineTargetClass : Attribute
{
static bool? _inherited;
/// <summary>
/// Is Inherited Property of AttributeUsage
/// </summary>
public static bool Inherited
{
get
{
if (_inherited != null) return _inherited.Value;
var attr = typeof(CommandLineAttr).GetCustomAttributes(typeof(AttributeUsageAttribute), true)[0] as AttributeUsageAttribute;
_inherited = attr.Inherited;
return _inherited.Value;
}
}
}
機能を追加する
必須オプションを設定できるオプション名を自由に設定できるpublic sealed class CommandLineParser
{
}
必須オプションの有無を確認する機能
Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.
IEnumerable<MemberInfo> getAllCommandLineMember()
{
var memlist = new List<MemberInfo>();
memlist.AddRange(_targetType.GetRuntimeFields());
memlist.AddRange(_targetType.GetRuntimeProperties());
foreach (var member in memlist)
{
if (member.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) == null) continue;
yield return member;
}
}
IEnumerable<CommandLineAttr> getCLAttrs()
{
foreach (var mem in getAllCommandLineMember())
{
yield return mem.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) as CommandLineAttr;
}
}
const string c_Prefix = "-";
readonly string _pleaseHelp = $"\n\nPlease reference help that you specify \" {c_Prefix}h or {c_Prefix}help\"";
List<CommandLineAttr> _commandLineAttrList;
// 「CommandLineAttr」属性インスタンスを全部取得するプロパティ
List<CommandLineAttr> CommandLineAttrList
{
get
{
if (_commandLineAttrList != null) return _commandLineAttrList;
_commandLineAttrList = getCLAttrs().ToList();
return _commandLineAttrList;
}
}
// コマンドラインをチェックするメソッド
bool chkCommadLine()
{
// check required option
if (chkRequired(CommandLineAttrList) == false) return false;
return true;
}
// 必須オプションが指定されているかチェックするメソッド
bool chkRequired(IEnumerable<CommandLineAttr> allCommandLineAttrs)
{
foreach (var optName in allCommandLineAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}"))
{
if (Args.Contains(optName) == false)
{
ErrorMessage = $"Error: Option {optName} is not specified.{_pleaseHelp}";
return false;
}
}
return true;
}
コマンドラインオプションで指定された値をフィールドやプロパティに格納する
// プロパティ・フィールドに値を格納するメソッド
void setValueToPropAndField()
{
foreach (var mem in getAllCommandLineMember())
{
var option = $"{c_Prefix}{(mem.GetCustomAttribute(typeof(CommandLineAttr)) as CommandLineAttr).CLOptionName}";
Type memType;
if (mem is PropertyInfo pi)
{
memType = pi.PropertyType;
if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
pi.SetValue(null, newVal);
}
else if (mem is FieldInfo fi)
{
memType = fi.FieldType;
if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
fi.SetValue(null, newVal);
}
}
}
Dictionary<string, string> _optValueDic;
// 入力されたコマンドライン引数をキー、その時指定された内容をValueに持つ辞書のプロパティ
Dictionary<string, string> OptValueDic
{
get
{
if (_optValueDic != null) return _optValueDic;
_optValueDic = new Dictionary<string, string>();
var opts = CommandLineAttrList.Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
var beforeOpt = string.Empty;
foreach (var arg in Args)
{
if (opts.Contains(arg))
{
// if options are duplicated, Exceptin occur.
_optValueDic.Add(arg, string.Empty);
beforeOpt = arg;
}
else
{
_optValueDic[beforeOpt] = arg;
}
}
return _optValueDic;
}
}
// 格納前に値をキャストするメソッド
bool tryGetOptionValue(Type memberType, string option, out object val)
{
val = null;
// implement other type after days ...
if (memberType == typeof(int))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = int.Parse(value);
}
else if (memberType == typeof(double))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = double.Parse(value);
}
else if (memberType == typeof(string))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = value;
}
else if (memberType == typeof(bool)) val = OptValueDic.TryGetValue(option, out _);
else return false;
return true;
}
今までの2機能のまとめ
public CommandLineParser(Type targetType)
{
_targetType = targetType;
if (_targetType == null) throw new ArgumentNullException();
if (isDefineCommandTargetAttr() == false) throw new Exception($"Type[{_targetType.Name}] does not have {nameof(CommandLineTargetClass)}.");
Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.
IsError = chkCommadLine() == false;
// implement checking args method after days ...
setValueToPropAndField();
}
// CommandLineTargetClass 属性が存在するかどうか
bool isDefineCommandTargetAttr()
{
return _targetType?.GetCustomAttributes(typeof(CommandLineTargetClass), CommandLineTargetClass.Inherited).Length != 0;
}
ヘルプメッセージを出力する機能
// ヘルプメッセージのプロパティ
public string HelpMessage
{
get
{
var sb = new StringBuilder();
sb.AppendLine(getAssemblyInfo());
sb.Append($"usage: {getAssemblyName()} ");
var allAttrs = CommandLineAttrList;
var reqOpts = allAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
if (reqOpts.Count != 0)
{
foreach (var reqOpt in reqOpts)
{
sb.Append($"{reqOpt} {{ param }} ");
}
}
sb.AppendLine($"[ options ... ]");
sb.AppendLine();
foreach (var attr in allAttrs.OrderBy(x => x.CLOptionName.Length))
{
sb.AppendLine($"{c_Prefix}{attr.CLOptionName}\t: {attr.HelpMessage}");
}
sb.AppendLine();
sb.AppendLine("Display this message.");
sb.AppendLine($"{c_Prefix}h");
sb.AppendLine($"{c_Prefix}help");
return sb.ToString();
}
}
// アセンブリに設定された「ツール名」「バージョン」「会社名」「著作権」「ツールの説明」を抜き出して文字列にするメソッド
string getAssemblyInfo()
{
var assembly = Assembly.GetAssembly(_targetType);
if (assembly == null) return string.Empty;
var sb = new StringBuilder();
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
// FileDescription is Title.
sb.AppendLine($"{fvi.FileDescription} {fvi.FileVersion} {fvi.CompanyName} {fvi.LegalCopyright}");
sb.AppendLine(fvi.Comments);
return sb.ToString();
}
// アセンブリに設定されたアプリケーションの名前を取得するメソッド
string getAssemblyName()
{
var assembly = Assembly.GetAssembly(_targetType);
if (assembly == null) return string.Empty;
return assembly.GetName().Name;
}
ヘルプメッセージかどうかの判定
public bool IsHelp
{
get
{
if (Args == null || Args.Any() == false) return false;
return Args.Any(x => x == $"{c_Prefix}h" || x == $"{c_Prefix}help");
}
}
CommandLineParser の全体コード
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CLOptionLib
{
/// <summary>
/// command line parser
/// </summary>
public sealed class CommandLineParser
{
#region private member
Type _targetType;
const string c_Prefix = "-";
readonly string _pleaseHelp = $"\n\nPlease reference help that you specify \" {c_Prefix}h or {c_Prefix}help\"";
Dictionary<string, string> _optValueDic;
List<CommandLineAttr> _commandLineAttrList;
#endregion
#region property
/// <summary>
/// Arguments
/// </summary>
public IEnumerable<string> Args { get; }
/// <summary>
/// Did Command Line Error occur.
/// </summary>
public bool IsError { get; private set; }
/// <summary>
/// error message
/// </summary>
public string ErrorMessage { get; private set; }
/// <summary>
/// exist help command
/// </summary>
public bool IsHelp
{
get
{
if (Args == null || Args.Any() == false) return false;
return Args.Any(x => x == $"{c_Prefix}h" || x == $"{c_Prefix}help");
}
}
/// <summary>
/// help message
/// </summary>
public string HelpMessage
{
get
{
var sb = new StringBuilder();
sb.AppendLine(getAssemblyInfo());
sb.Append($"usage: {getAssemblyName()} ");
var allAttrs = CommandLineAttrList;
var reqOpts = allAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
if (reqOpts.Count != 0)
{
foreach (var reqOpt in reqOpts)
{
sb.Append($"{reqOpt} {{ param }} ");
}
}
sb.AppendLine($"[ options ... ]");
sb.AppendLine();
foreach (var attr in allAttrs.OrderBy(x => x.CLOptionName.Length))
{
sb.AppendLine($"{c_Prefix}{attr.CLOptionName}\t: {attr.HelpMessage}");
}
sb.AppendLine();
sb.AppendLine("Display this message.");
sb.AppendLine($"{c_Prefix}h");
sb.AppendLine($"{c_Prefix}help");
return sb.ToString();
}
}
/// <summary>
/// command line attr of target type
/// </summary>
List<CommandLineAttr> CommandLineAttrList
{
get
{
if (_commandLineAttrList != null) return _commandLineAttrList;
_commandLineAttrList = getCLAttrs().ToList();
return _commandLineAttrList;
}
}
/// <summary>
/// Key: option
/// <para>Value: argument value</para>
/// </summary>
Dictionary<string, string> OptValueDic
{
get
{
if (_optValueDic != null) return _optValueDic;
_optValueDic = new Dictionary<string, string>();
var opts = CommandLineAttrList.Select(x => $"{c_Prefix}{x.CLOptionName}").ToList();
var beforeOpt = string.Empty;
foreach (var arg in Args)
{
if (opts.Contains(arg))
{
// if options are duplicated, Exception occur.
_optValueDic.Add(arg, string.Empty);
beforeOpt = arg;
}
else
{
_optValueDic[beforeOpt] = arg;
}
}
return _optValueDic;
}
}
#endregion
#region Get assembly name
/// <summary>
/// Get assembly name
/// </summary>
/// <returns>assembly name of exe</returns>
string getAssemblyName()
{
var assembly = Assembly.GetAssembly(_targetType);
if (assembly == null) return string.Empty;
return assembly.GetName().Name;
}
#endregion
#region Get assembly info
/// <summary>
/// Get assembly info.
/// <para>Tool Title</para>
/// <para>Tool Version</para>
/// <para>CompanyName</para>
/// <para>LecalCopyright</para>
/// <para>Comments(tool description)</para>
/// </summary>
/// <returns>string : {title version company legalCopyright \n comments}</returns>
string getAssemblyInfo()
{
var assembly = Assembly.GetAssembly(_targetType);
if (assembly == null) return string.Empty;
var sb = new StringBuilder();
var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
// FileDescription is Title.
sb.AppendLine($"{fvi.FileDescription} {fvi.FileVersion} {fvi.CompanyName} {fvi.LegalCopyright}");
sb.AppendLine(fvi.Comments);
return sb.ToString();
}
#endregion
#region ctor
/// <summary>
/// ctor
/// </summary>
/// <param name="targetType">Type with specified CommandLineTargetClass Attribute</param>
/// <exception cref="ArgumentNullException"/>
/// <exception cref="Exception"/>
public CommandLineParser(Type targetType)
{
_targetType = targetType;
if (_targetType == null) throw new ArgumentNullException();
if (isDefineCommandTargetAttr() == false) throw new Exception($"Type[{_targetType.Name}] does not have {nameof(CommandLineTargetClass)}.");
Args = Environment.GetCommandLineArgs()[1..]; // First arg is EXE file name.
IsError = chkCommadLine() == false;
// implement checking args method after days ...
setValueToPropAndField();
}
#endregion
#region Check having CommandLineTargetClass Attribute
/// <summary>
/// check having CommandLineTargetClass Attribute.
/// </summary>
/// <returns>true: has CommandLineTargetClass, false: does not have CommandLineTargetClass</returns>
bool isDefineCommandTargetAttr()
{
return _targetType?.GetCustomAttributes(typeof(CommandLineTargetClass), CommandLineTargetClass.Inherited).Length != 0;
}
#endregion
#region Check Command Line Options
/// <summary>
/// Check Command Line Options.
/// </summary>
/// <returns>true: OK, false: NG</returns>
bool chkCommadLine()
{
// check required option
if (chkRequired(CommandLineAttrList) == false) return false;
return true;
}
#endregion
#region Get Command Line Attrs
/// <summary>
/// Get Command Line Attrs of MemberInfo
/// </summary>
/// <returns>Command Line Attr Objects</returns>
IEnumerable<CommandLineAttr> getCLAttrs()
{
foreach (var mem in getAllCommandLineMember())
{
yield return mem.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) as CommandLineAttr;
}
}
#endregion
#region Get Command Line Member
/// <summary>
/// Get Command Line Member.
/// </summary>
/// <returns></returns>
IEnumerable<MemberInfo> getAllCommandLineMember()
{
var memlist = new List<MemberInfo>();
memlist.AddRange(_targetType.GetRuntimeFields());
memlist.AddRange(_targetType.GetRuntimeProperties());
foreach (var member in memlist)
{
if (member.GetCustomAttribute(typeof(CommandLineAttr), CommandLineAttr.Inherited) == null) continue;
yield return member;
}
}
#endregion
#region check required option
/// <summary>
/// Check required option
/// </summary>
/// <param name="allCommandLineAttrs">All Command Line Attrs</param>
/// <returns>true: OK, false: NG</returns>
bool chkRequired(IEnumerable<CommandLineAttr> allCommandLineAttrs)
{
foreach (var optName in allCommandLineAttrs.Where(x => x.Required).Select(x => $"{c_Prefix}{x.CLOptionName}"))
{
if (Args.Contains(optName) == false)
{
ErrorMessage = $"Error: Option {optName} is not specified.{_pleaseHelp}";
return false;
}
}
return true;
}
#endregion
#region set value to property and field
/// <summary>
/// set value to property and field
/// </summary>
void setValueToPropAndField()
{
foreach (var mem in getAllCommandLineMember())
{
var option = $"{c_Prefix}{(mem.GetCustomAttribute(typeof(CommandLineAttr)) as CommandLineAttr).CLOptionName}";
Type memType;
if (mem is PropertyInfo pi)
{
memType = pi.PropertyType;
if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
pi.SetValue(null, newVal);
}
else if (mem is FieldInfo fi)
{
memType = fi.FieldType;
if (tryGetOptionValue(memType, option, out var newVal) == false) continue;
fi.SetValue(null, newVal);
}
}
}
#endregion
#region option value after cast type
/// <summary>
/// option value after cast type.
/// </summary>
/// <param name="memberType">member(property or field) type</param>
/// <param name="option">option name(Key of OptValueDic)</param>
/// <param name="val">out value</param>
/// <returns>true: success getting, false: fail getting</returns>
bool tryGetOptionValue(Type memberType, string option, out object val)
{
val = null;
// implement other type after days ...
if (memberType == typeof(int))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = int.Parse(value);
}
else if (memberType == typeof(double))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = double.Parse(value);
}
else if (memberType == typeof(string))
{
if (OptValueDic.TryGetValue(option, out var value) == false) return false;
val = value;
}
else if (memberType == typeof(bool)) val = OptValueDic.TryGetValue(option, out _);
else return false;
return true;
}
#endregion
}
}
まとめ
XAML: カスタムコントロール(今風なToggleButton)
いよいよもってカスタムコントロールである。ControlTemplate の知識等の集大成に近い。 なお、ほかにもいろいろな知識がいるので、結局こいつは難しい部類の印象。 そして、これをまとめるからには「ResourceDictionary」もまとめないといけない。 実のところ、外観だけなら「ResourceDictionary」のが適していると思う。
カスタムコントロールを使う場面は「独自のバインディングターゲットを作りたい」、これに尽きると思う。 このバインディングターゲットを作りたいというのは、「元のプロパティが依存関係プロパティじゃないから、カスタムコントロールで仕方なく依存関係プロパティにしなくちゃいけない」も含まれる。
独自バインディングターゲットはいらない、ってなれば、まぁResourceDictionaryでいいかなぁと個人的には思う。
目次
カスタムコントロールというのは、言ってしまえば自作コントロールである。
ユーザコントロールとちょっと似ているけれど、明確に違うところがある。 しれっと ResourceDictionary いるけどこれも入れておいた方がよさそうだったので入れた。
他にも差異がありそうな項目がいそうだが、とりあえずこれだけで。 見てのとおり、1点明確な違いはName属性が付与できるかできないかである。
Name属性については過去にまとめた。 backacheengineer.hatenablog.com したがって、Name属性をつけられるという点においてはカスタムコントロールはライブラリ向きということになる。
そのツール独自の画面でいい、となるとまぁユーザコントロールでよいだろう。
バインディングターゲットが必要ないのであれば、ユーザコントロールである必要もなく、ResourceDictionary を使えばよい。 デザインを作るのは困難だと思う。参考になるデザインは他のツールか、
Windowsの設定とかのデザインを見ればなんとなーくふわっと「こんなの作りたい」というのは出てくる。
けれども、ControlTemplate でそれを実現しようとなると普通に難しいという・・・。 そういう時には generic.xaml というファイルが下記フォルダにいるはずなので参考になる(はず)。
途中の番号部分はたぶんwin10のビルド番号かなんかなので、人によって違う。 C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.19041.0\Generic こいつには VisualStateManager とか ツールのテーマカラーとかがなんと定義されているのだ。
おーすごい。ただ量もえげつない。でも参考になる(はず)。 ということでカスタムコントロールで今風な ToggleButton を作ってみる。
普通の ToggleButton だと押してある状態がON、そうでないときOFFみたいなボタンなんだけれども、
今風だとなんかバーになってると思う。それを作る。 既に作ったのでまずは結果から。まだまだ改良の余地ありなやつだが、とりあえずできたんで・・・。
必要な機能は以下のとおり。実際に使う場合も含めて考えてる。 1 は TextBlock をそのまま使う。 テキストの切り替えは Trriger でサクッと。 2 はソースとなる変数を作って設定する。ターゲットは「Visibility」というenum。
これなら表示状態、非表示状態、非表示かつ描画領域非確保状態を扱える。 3 はソースとなる変数を作って設定する。ターゲットはもちろん Text である。 4 は内部コード内で初期化する。 5 は Slider を使って実装していく。 6 は Slider の外観を変更して対応する。 7 はどうしようか悩んだ結果、透明な ToggleButton をバーにかぶせることにした(おい)。
これでクリック動作は確保できるので、あとは VisualState を実装してやって、自分で動かす。 まずはXAMLから。
まとめていて気が付いたけど、これサイズが変更できないのでは・・・。とりあえずこのままで。
本当に改善の余地が多いなぁ。 コメントが一切ないのは、カスタムコントロールに対してコピペする可能性を考慮した。
日本語含むカスタムコントロールの「Generic.xaml」はビルドに失敗するんで。
(1文足せば日本語も使える。単純に「UTF-8」しかできないのが問題で、なんか「shift-jis」もいけるように1文だけ足せばいいはず) 続いてC#コード。
ここで注意すべきなのが、「バインディングソースは依存関係プロパティじゃなくていいこと」。
別に依存関係プロパティで定義してもいいんだけど、無駄なコードは避けておきたい。
継承元が ContentControl なので、IsChecked を実装している。
実は ToggleButton を継承すると IsChecked は独自実装する必要ないんだけど、VisualState の操作がうまくできなかった。
力不足だ。この継承をうまいこと使えれば、たぶんだが VisualState をもっと楽できる。 こんなもんだろうか。
ControlTemplate は本当に難しい。
今回の実装も、正直微妙だなと思っている。
うまいこと継承とか使いこなしたいところ。 あと、もっと今風にするなら丸くしたいなと。
Thumb コントロールの外観を丸くして、Rectangle の角を丸くすればもっと今風になるなって。
デザインは奥が深い。プログラマもデザインする時代となると、もっと本読まないといけないかもね。カスタムコントロール
機能
バインディングターゲット作成
Name属性付与
内部コード実装
外観の実装
カスタムコントロール
○
○
○
○
ユーザコントロール
○
×
○
○
ResourceDictionary
×
○
×
○
デザインを作るうえでの参考
今風な ToggleButton を作る
必要な機能
XAML・C#コード
<ControlTemplate x:Key="sliderTemplate" TargetType="Slider">
<Grid Width="70" Height="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Name="checkedRect"
Grid.Column="2"
Fill="Red">
</Rectangle>
<Thumb Name="barThumb"
Grid.Column="1"
Width="24">
</Thumb>
<Rectangle Name="uncheckedRect"
Grid.Column="0"
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}, Path=Value}"
Fill="Blue"/>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type local:ModernToggle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ModernToggle}">
<Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Margin="{TemplateBinding Margin}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckedState">
<VisualState x:Name="UnChecked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="slider"
Storyboard.TargetProperty="Value"
To="0" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="slider"
Storyboard.TargetProperty="Value"
To="46" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Name="stateText"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=UnCheckedStateText}"
FontSize="{TemplateBinding FontSize}"
Margin="24"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=StateTextVisibility}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Slider Grid.Column="1"
Name="slider"
Template="{StaticResource sliderTemplate}"
Value="0"
Maximum="46"/>
<ToggleButton Name="toggle"
IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsChecked}"
Grid.Column="1"
Opacity="0"/>
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=toggle, Path=IsChecked}" Value="True">
<Setter TargetName="stateText" Property="Text" Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=CheckedStateText}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
public class ModernToggle : ContentControl
{
public event EventHandler CheckedChanged;
/// <summary>
/// text of checked state
/// </summary>
public string CheckedStateText { get; set; } = "ON";
/// <summary>
/// text of unchecked state
/// </summary>
public string UnCheckedStateText { get; set; } = "OFF";
/// <summary>
/// visibility statetext
/// </summary>
public Visibility StateTextVisibility { get; set; } = Visibility.Visible;
/// <summary>
/// dependencyproperty IsChecked
/// </summary>
public static DependencyProperty IsCheckedProperty { get; private set; }
/// <summary>
/// true: Checked, false: Unchecked
/// </summary>
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
/// <summary>
/// static constructor
/// </summary>
static ModernToggle()
{
IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ModernToggle), new PropertyMetadata(false, OnCheckedChanged));
}
/// <summary>
/// event of CheckedChanged for DependencyProperty
/// </summary>
static void OnCheckedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
(obj as ModernToggle).OnCheckedChanged(EventArgs.Empty);
}
/// <summary>
/// event of CheckedChanged
/// </summary>
protected virtual void OnCheckedChanged(EventArgs args)
{
VisualStateManager.GoToState(this, IsChecked == true ? "Checked" : "UnChecked", true);
CheckedChanged?.Invoke(this, args);
}
}
まとめ
XAML: Templateについて その2
やっとこさ ControlTemplate が終わったんでまとめる。
目次
まずは簡単なおさらいからで、ControlTemplate でできることは「外観の再定義」である。
DataTemplate でも外観の変更はできるのだけど、ControlTemplate はバインディング等も改造できるので、こちらの方ができることが多い。
DataTemplate は、バインディングする際に親定義から探してバインドするといったことができない。
つまり、TemplateBinding や RelativeSource の Mode=TemplatedParent が使えない。 ControlTemplate は Template プロパティに対して定義していく。 まずは 簡単に ControlTemplate を適用したものを載せる。 上記の xaml コードは以下のような結果になる。
見てわかるとおり、ただの TextBlock にしか見えない。たとえ Button 側に BorderBrush や BorderThickness を設定しても、うまいこと ControlTemplate に反映してくれるわけではないということである。しかも表示されているテキストは Button 側に設定したものではない。これではボタンとして見てわからないので、当然改造しなければならない。
かといって、ControlTemplate 内に定義している Border 等に決め決めで設定しても、その色やテキストしか使えず、使い物にならない。 これを解決するのが何度か出てきている TemplateBinding である。 TemplateBinding は前章で説明した内容を解決するものである。これは、「ControlTemplate を適用するコントロールのプロパティをバインディングソースにする」ということである。 つまり、TemplateBinding を使用すると下記のことを実現できる。 もちろん、上記以外にもいろいろ実現できる。HorizontalAlignment も VerticalAlignment もバインディングソースにできる。
Button 側に設定した内容を ControlTemplate に引き継げるのである。 xaml コードを見ていく。 そして、適用した結果の画像が下記である。
見てのとおり、枠線の色と太さ、それとテキストが見事に反映されている。
ここで、Text へのバインディングが TemplateBinding でないことに気が付く。
これは、Buttun の Content プロパティを TwoWay でバインディングしなければいけないのが理由である。
TemplateBinding は OneWay のバインディングを前提としており、TwoWay のバインディングができないためだ。
そして、TwoWay でのバインディングかつ TemplateBinding と同じ効果をもたらすのが RelativeSource による Binding というわけである。
正確に書くと、「Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay, Path=Content」といったように、Mode の選択が可能なのである。 ControlTemplate にバインドするプロパティが TwoWay でバインドしないといけないのかを見極める必要がある。
なお、OneWay の場合は RelativeSource でもよい。「Binding RelativeSource={RelativeSource Mode=TemplatedParet}, Mode=OneWay, Path=....」の省略形が「TemplateBinding」なのである。
(なお、ぶっちゃけ TwoWay が必要なやつで TemplateBinding するとエラーになったりするのでそのタイミングで RelativeSource の書き方を試せばいい・・・) しかし、ここで気づくかもしれないが、実は外観はできても「ボタンを押したときの動作が見えない」のだ。
これは VisualState という状態遷移を考慮していないためである。この状態遷移を考慮するうえで必要なのが VisualStateManager だ。 VisualStateManager は外観状態を管理するものである(そのまんま)。注意したいのが、この VisualState はコントロールごとにあること。
今回の例に挙げ続けている Button コントロールは2グループの VisualStateGroup からなる。 すべての状態を厳密に決める必要はない。特に動作がなくていい状態は空の指定ができるためだ。
そして、これまでの xaml コードはボタンの Template を直接いじくってきたが、ここにきてリソースにおけるように Style として定義していく。
Style としてリソースに定義しておけば、すべてのボタンに対して同じ外観、VisualState を反映できる。 以下が Style の xaml コードだ。一気に長くなるが、下記の実装になる。 xaml コードを見ればわかるが、VisualState はプロパティの値を上書きしているだけである。なお、この上書きに対してはアニメーションを用いることが可能である。今回はアニメーションを使わず、瞬時にプロパティを上書きしていく。 このスタイルを使って簡単なデモをする。 デモ内容 まずは結果から。
ちゃんとデモ内容どおりに外観が変化していることがわかる。
VisualSteteManager で状態を再定義してやれば、馴染みの動作になるというわけである。
なお、真ん中のボタンを押したときの画像は割愛する。 このデモに使った xaml コードと C# コードも載せておく。 xaml コード(スタイル部分は省略している。) C# コード 次はこの ControlTemplate の知識を使って「カスタムコントロール」を作っていく。
この知識があって初めて、カスタムコントロールが自在に作れるようになるのだ。ControlTemplate でできること
基本的な ControlTemplate
<Button Content="Click me!"
Click="Button_Click"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="0"
Grid.Column="1"
BorderBrush="Red"
BorderThickness="2">
<Button.Template>
<ControlTemplate>
<Border>
<TextBlock Text="temporary"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
TemplateBinding
<Button Content="Click me!"
Click="Button_Click"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Grid.Row="0"
Grid.Column="2"
BorderBrush="Red"
BorderThickness="2">
<Button.Template>
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Content}"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
VisualStateManager
<Style x:Key="btnStyle3" TargetType="Button">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="12">
<!--#region 状態定義 -->
<!-- 各状態の定義がないと遷移できない。 -->
<!-- また、この定義はすべて書いておかないといけない。なお、不要なものはタグを空にしておけばよい。 -->
<!-- さらには、定義場所も注意がいる。下手すると全く反映されない。 -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver"/>
<!-- ボタン押下時の色変更 -->
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="border"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<SolidColorBrush Color="LightBlue"/>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!-- 操作不可時、灰色の Rectangle で覆う -->
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="disabledRect"
Storyboard.TargetProperty="Visibility">
<!-- DiscreteObjectKeyFrame : 特定の値に直接変更するやつ -->
<!-- キーフレームは、アニメーションが開始されてから特定の時間が経過したときのターゲットプロパティの値を示す -->
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusedStates">
<VisualState x:Name="Unfocused"/>
<!-- フォーカスした際、瞬時に点線の透明度を 1 にするアニメーション -->
<VisualState x:Name="Focused">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="focusRectangle"
Storyboard.TargetProperty="Opacity"
To="1" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerFocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<!--#endregion-->
<Grid>
<!-- 内容の本体 -->
<ContentPresenter Name="contentpresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"/>
<!-- フォーカス時の点線用Rectangle -->
<Rectangle Name="focusRectangle"
Opacity="0"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1"
StrokeDashArray="2 2"
Margin="4"
RadiusX="12"
RadiusY="12"/>
<!-- 操作不可時の覆い隠す用Rectangle -->
<Rectangle Name="disabledRect"
Fill="Black"
Visibility="Collapsed"
Opacity="0.5"
RadiusX="12"
RadiusY="12"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Button Content="Disable right btn"
Click="Button_Click_1"
Grid.Column="0"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
<Button Name="operateBtn"
Content="btn"
Click="Button_Click"
Grid.Column="1"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
<Button Content="Enable left btn"
Click="Button_Click_2"
Grid.Column="2"
Padding="12"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Style="{StaticResource btnStyle3}"/>
</Grid>
public partial class ButtonTemplateDemo : Window
{
public ButtonTemplateDemo()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("Click!");
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
operateBtn.IsEnabled = false;
}
private void Button_Click_2(object sender, RoutedEventArgs e)
{
operateBtn.IsEnabled = true;
}
}
まとめ