前提:
SQLから受付履歴を日付等で抽出して下表のように一覧表示するWPFプログラムがあるとします。
受付№ | 製品番号 | 名前 |
---|---|---|
20220101000201 | A0001 | 製品1 |
20220101000101 | B0005 | 製品5 |
20220101000202 | B0006 | 製品6 |
20220101000301 | 01234 | 機械1 |
20220101000302 | 04567 | 機械2 |
20220101000303 | 01234 | 機械1 |
20220101000304 | FUMEI | 不明 |
20220101000401 | A0001 | 製品1 |
20220101000501 | C0001 | 製品3 |
20220101000502 | X1234 | ソフト1 |
20220101000503 | 00001 | マシン1 |
※qiitaのマークダウン記法で作表したので1行おきに背景色変わってますが実際のプログラムではそんな気の利いた事はしていません。
製品番号とか名前はフィクションですが、受付番号は「日付」(8桁)+「通番」(4桁)+「通番内の連番」(2桁)の14桁構成です。
表は「同一通番であれば同一案件で複数機器の受付をした」みたいな内容となっています。
通常同一案件で同じ製品番号を登録することがないため、同一通番内で同じ製品番号が登録されているときは入力ミスの可能性が高いので、その注意喚起をしたいという事でタイトルのような要望がありました。
実装(混乱編):
最初に思いついたのはSQL側のViewで重複数を格納する列を用意する方法でしたが、列の定義をどう書いていいのかさっぱり全く想像がつかないことに気付いて他のアイディアを検討する事にしました。
次に思いついてしまったのが、「だったらWPFのDataTableにDataContextとして持たせているDataTebleから重複数を表示するリストを作れば良くない?」でした。
通番内で製品番号が重複しているかどうかを調べるのは「【受付№】から【通番内連番】を削除した文字列」+「製品番号」のリストを作って、その各行の文字列をリスト全体で数えたらいけそうです。
「【受付№】から【通番内連番】を削除した文字列」は単純に受付№を左から12文字切り出せばいいので簡単簡単。
List<string> duplicationChickList = DataTeble.AsEnumerable().Select(i => i["受付№"].ToString().Substring(0,12) + i["製番"].ToString()).ToList();
で、このリストから各行の文字列が当該リスト内に何個あるかカウントすればええんや!……それの結果はどうすんの?
結果のリストを作ったらいいんやで!(五月雨式にどんどん駄目になって行ってる感じはこの時点で気付いてましたが、実装を優先してしまいました)。
当初は下のリストの前に重複個数のリスト作って、それを評価するというこれ以上ない冗長っぷりでしたが、流石にそこは纏めれらると気づいて途中で以下のようにしました。
List<bool> duplicationResultList = new List<bool>();
foreach (string item in duplicationChickList)
{
//重複リスト内で重複する場合結果リストにtrueを、そうでない場合はfalseを登録するif文
if (duplicationChickList.Count(n => n == item) > 1)
{
duplicationResultList.Add(true);
}
else
{
duplicationResultList.Add(false);
}
}
そして、上記の結果リストduplicationResultListを元にDataGridRowのBackgroundを変更するコードを書きました。当初nullしか返ってこなかったのでWebで調べて
DataGrid.UpdateLayout();
DataGrid.ScrollIntoView(DataGrid.Items[i]);
を追加しています。
for (int i = 1; i < duplicationResultList.Count; i++)
{
DataGrid.UpdateLayout();
DataGrid.ScrollIntoView(DataGrid.Items[i]);
DataGridRow dgr = (DataGridRow)DataGrid.ItemContainerGenerator.ContainerFromIndex(i);
dgr.Background = duplicationResultList[i]?new SolidColorBrush(Colors.MistyRose):new SolidColorBrush(Colors.White);
}
酷い発想ですが、これで取りあえず実装できてしまって、しかも(一見)意図した結果が表示されてしまいました。
しかしこれ、どうやら描画が終わったDataGridに対して1行ずつ背景色を変更しているようで、行が多くなると勝手にスクロールして最終行が表示されてしまい、先頭行が隠れてしまうという副作用がありました。
それは大した問題じゃないと目を瞑るとしても、取得行数が増えると全然関係ない行の背景色まで変更されてしまう事が判明してしまい、酷い発想のロジックには不具合つきものやな、と妙に感心してしまいました。
嫌まあ動作も遅せーし、こういうのはさっさと葬って「やっぱりxaml側でどうにかしたいよね」と言う当たり前と言えば当たり前の実装を検討する事にしました。
実装(解決編):
SQLから取得するDataTableに行ごとに重複数乗っけられたらなー(チラ)。乗っけられないかなー(チラッチラ)。「WPF DataTable 重複数」とか「SQL 行ごとのデータが重複しているか」「SQL 行のデータの重複を各行に集計」とか検索ワードいろいろ工夫してみたけどGoogleは結局GROUP化しか教えてくれねえんで今回のケースでは全然使えない。
重複してるデータを纏めたくねえんだよ!!
これは人力検索、もう人力検索に頼るしか無い、無理なら無理で糞実装で諦めるしかないから可否だけでも教えてくれい!と言う事でQiitaで相談してみることにしました。
(カタカタ)「SQLで各行の特定カラムの値が、そのカラム内に何個あるかカウントして表示したい。」っと……
「とりあえず、グループ化について調べましょう。」
嫌だからそうじゃねえんだって(´・ω・`)表迄付けて重複をまとめずにって判るようにしてんのになんでそんな回答できんだよ……おまえはGoogleか?
としょんぼりしてたところに2件目の回答が。
ウィンドウ関数を使うと良いかもしれません
この1行でなんか正解っぽい感じ、そしてガッチリコード迄記載していただいていて正にこれ。凄い!
「ウィンドウ関数」を調べたら「当該行に関係するテーブル行の集合に対して計算する関数」と言う事でド的中間違いなしじゃないですか!やっぱりそういうの用意されてるんだけSQL!これじゃん!あんじゃんよもー。
私の原始人みたいな検索文字列では全然引っかからなかった至宝の回答がここにありました!本当にありがとうございます。
つまりはこう
SELECT *,COUNT(*) OVER(PARTITION BY (LEFT([受付№],12)+[製品番号])) AS duplicationCount FROM [テーブル名] WHERE ……
これで列「duplicationCount」に各行の重複数が取れたので、DataGridのプロパティにトリガー設定してやればいいじゃん?
<DataGrid ItemsSource="{Binding}">
<DataGrid.ItemContainerStyle>
<Style TargetType="DataGridRow">
<!-- 行のデフォルト背景色(製番が重複している行だけこの背景色が残る) -->
<Setter Property="Background" Value="MistyRose" />
<!-- 製番が重複していない行の背景色(重複数列の値が[1]の場合の背景色) -->
<Style.Triggers>
<DataTrigger Binding="{Binding duplicationCount}" Value="1">
<Setter Property="Background" Value="white"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.ItemContainerStyle>
……
で良いじゃん!
DataTriggerのValueでは「以外」の設定できないからデフォルト背景色を「重複している行の色」にして、DataTriggerで「Value="1"」(これは即ち当該列に文字列が1件しかない=重複なしの場合と言う意味)の場合は背景色を「白」にするという逆転チェストで対応しました。
結論:
ウィンドウ関数凄い!教えていただいたほしいもさん(@hoshiimo_se)には感謝感謝大感謝なのですよ。