🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Q&A

解決済

2回答

2470閲覧

完全コンストラクタパターンのコレクションをDataGridで編集するには

kawaguti

総合スコア9

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

1グッド

0クリップ

投稿2020/11/23 03:05

社内でツールなどを作っている者です、他に訪ねる宛てもなく
この方法が正しいのか間違っているのかわかりません
一般的な方法など教えて頂ければと思います

C# + WPF + Prism な環境で DDD的手法で開発を行っています

DataGridにデータクラスのコレクションをバインドして表示、編集を行いたいと考えております

データクラスは完全コンストラクタパターンにするため、各プロパティは読み取り専用ですので
ViewでのバインディングモードをOneWayやOneTimeとしたところテキストボックスの編集は可能ですが
DataGridのRowChanged後に編集前のテキストに戻ってしまいます

幸い入力値はDataGridのLostFocusイベントのRoutedEventArgs引数のOriginalSourceプロパティにありましたので
これを元にデータクラスを新たに作成しDataGridのItemSourceを再指定することで、データクラス、Viewが入力したものに
なることをできたのですが、Viewの編集が無効になる点、LostFocusイベントでしか入力値を取得出来ない点を考えると
そもそもDataGridでの編集に読み取り専用のデータクラスは利用しないほうがいいのかと思えます

その他に考えられる方法は
1DataGridの編集にのみ利用するクラスなのだから読み取り専用にしない
2読み書き可能なデータクラスを作っておき、ViewModeで載せ替える
ぐらいしか思いつかないのですが、このような場合はどのようにするのが一般的なのでしょうか

TN8001👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Zuishin

2020/11/23 03:07

編集するのに読み取り専用にする意味がわかりません。
kawaguti

2020/11/23 03:14

読み取り専用とすることで他からのデータの変更をできないようにしたいのです DataGrid用に書き可能な点のみ違うクラスを別途作成ほうが良いのでしょうか
guest

回答2

0

ベストアンサー

私はアマチュアなので一般的な方法はわかりませんが、C# 9.0でレコード型が入ったことですしちょっとやってみました。

  • C# 9.0以上
  • Prism(面倒なのでDIしません)
  • ReactiveProperty(後片付けしてません)

xml

1<Window 2 x:Class="Questions305906.Views.MainWindow" 3 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 4 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 5 Width="525" 6 Height="350"> 7 <DockPanel> 8 <StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> 9 <Button Command="{Binding AddCommand}" Content="アイテム追加" /> 10 <Button Command="{Binding DispCommand}" Content="モデル確認" /> 11 </StackPanel> 12 <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Items}"> 13 <DataGrid.Columns> 14 <DataGridTextColumn Binding="{Binding Id}" Header="ID" IsReadOnly="True" /> 15 <DataGridTextColumn Width="200" Binding="{Binding Name}" Header="名前" /> 16 <DataGridTextColumn Width="100" Binding="{Binding Price}" Header="値段" /> 17 </DataGrid.Columns> 18 </DataGrid> 19 </DockPanel> 20</Window>

cs

1namespace Questions305906.Models 2{ 3 using System.Collections.ObjectModel; 4 using System.Diagnostics; 5 using System.Linq; 6 7 public record ItemModel(int Id, string Name, int Price); 8 9 public class Provider 10 { 11 public ReadOnlyObservableCollection<ItemModel> Items { get; } 12 private readonly ObservableCollection<ItemModel> items; 13 14 public Provider() 15 { 16 items = new() 17 { 18 new(Id: 1, Name: "aaa", Price: 123), 19 new(Id: 2, Name: "bbbb", Price: 456), 20 new(Id: 3, Name: "ccccc", Price: 7890), 21 }; 22 23 Items = new(items); 24 } 25 26 public void Replace(ItemModel oldValue, ItemModel newValue) 27 { 28 var index = items.IndexOf(oldValue); 29 items.Remove(oldValue); 30 items.Insert(index, newValue); 31 } 32 33 public void Add() 34 { 35 var id = items.Max(x => x.Id) + 1; 36 items.Add(new(Id: id, Name: "", Price: 0)); 37 } 38 39 public void Disp() 40 { 41 foreach (var item in items) Debug.WriteLine(item); 42 } 43 } 44} 45 46namespace Questions305906.ViewModels 47{ 48 using Prism.Mvvm; 49 using Questions305906.Models; 50 using Reactive.Bindings; 51 using Reactive.Bindings.Extensions; 52 using System; 53 54 public class ItemViewModel : BindableBase 55 { 56 public int Id { get; } 57 58 private string _Name; 59 public string Name { get => _Name; set => SetProperty(ref _Name, value); } 60 61 private int _Price; 62 public int Price { get => _Price; set => SetProperty(ref _Price, value); } 63 64 internal ItemModel Item; 65 internal ItemModel NewItem => Item = Item with { Name = Name, Price = Price }; 66 67 68 public ItemViewModel(ItemModel item) => (Item, Id, Name, Price) = (item, item.Id, item.Name, item.Price); 69 } 70 71 public class MainWindowViewModel : BindableBase 72 { 73 public ReadOnlyReactiveCollection<ItemViewModel> Items { get; } 74 public ReactiveCommand AddCommand { get; } = new(); 75 public ReactiveCommand DispCommand { get; } = new(); 76 77 private readonly Provider provider = new(); 78 79 80 public MainWindowViewModel() 81 { 82 Items = provider.Items.ToReadOnlyReactiveCollection(x => new ItemViewModel(x)); 83 Items.ObserveElementPropertyChanged().Subscribe(x => provider.Replace(x.Sender.Item, x.Sender.NewItem)); 84 85 AddCommand.Subscribe(() => provider.Add()); 86 DispCommand.Subscribe(() => provider.Disp()); 87 } 88 } 89} 90// .NET5でない場合 91//namespace System.Runtime.CompilerServices 92//{ 93// public class IsExternalInit { } 94//}

同値があるとおかしくなるのは目に見えているので、Idをユニークにしました。
DataGridのプレースホルダも使用しませんでした(方法はありそうですが未調査)

結局どうにかして詰め替えるしかないわけですが、ReactivePropertyのおかげでそれほど面倒というわけでもなかったです(MVMの詰め替え・Provider.Itemsの入れ替え、どちらも)
しかしこれは筋がいいとは思えないです(NewItemの雑さ^^;

レコード型が浸透してきてライブラリ等のサポートが入ってくれば、また変わると思います。まあこれからですね^^


追記
.NET Core 3.1でのcsproj(↑提示コードの最下段System.Runtime.CompilerServices.IsExternalInitのコメントを外してください)

xml:.csproj

1<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> 2 <PropertyGroup> 3 <OutputType>WinExe</OutputType> 4 <TargetFramework>netcoreapp3.1</TargetFramework> 5 <UseWPF>true</UseWPF> 6 <AssemblyName>Questions305906</AssemblyName> 7 <LangVersion>9.0</LangVersion> 8 </PropertyGroup> 9 <ItemGroup> 10 <PackageReference Include="Prism.DryIoc" Version="8.0.0.1909" /> 11 <PackageReference Include="ReactiveProperty" Version="7.5.1" /> 12 </ItemGroup> 13</Project>

投稿2020/11/23 17:05

編集2023/08/13 08:35
TN8001

総合スコア9855

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kawaguti

2020/11/24 02:55

NET5.0 ですか 私の職場は来年には10になりそうですが未だ7が現役なのでまったく調べていませんでした レコード型いいですね、これにWith式や提示して頂いたObserveElementPropertyChangedなどで載せ替えが簡単にできそうです 今できる対応としてReactivePropertyを積極的に使うようにしてみます 参考になるコードを提示していただきましてありがとうございます
TN8001

2020/11/24 08:58

念のため(私も最初間違ってしまったのですが^^; .NET5でなくても <LangVersion>9.0</LangVersion> とすれば使えます。 その際「IsExternalInit がないよ」と怒られますが、回答コード最後のように自分で空定義します。
kawaguti

2020/11/25 05:23 編集

言語使用だけなので Windows7で使えるということでしょうか 提示して頂いたコードをWindows10,Core2.1の環境でプロジェクトファイルを書き換えて実行して見ましたが、「リモート言語サーバー C#/Visual Basic 言語サーバー クライアント をアクティブにする処理でエラーが発生しました。 For more details, please examine the 以下ファイルパス」とエラーが出ました。 すごく興味はありますが本件とは外れてしまいますのでお構い無ければ参照先URLなど教えて頂けましたら助かります
TN8001

2020/11/25 09:09

WPFは .NET Core 3.0 からじゃなかったでしたっけ? [.NET Core 3.0 の新機能 | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0 .NET Core 3.1 .NET Framework 4.8 で(Prismレスで)ざっと確認しましたが動きました。 Windows7は手元にないので確認できませんが、おそらく動くと思うのですが。 record自体は不変なデータクラスを簡単に作れるようにするってだけですので、自分でクラスを書いても同じです。 C#9の確認がてら回答しましたが、 [C# 9.0 の新機能 - C# ガイド | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-9 * レコード * init 専用セッター(withで使用) * ターゲット型の新しい式(new()の事) を使わないように書き換えれば、C#9すら必要なくなります。 C#8版を上げてもいいのですが、せっかくスッキリしていたものをゴチャつかせるようで乗り気しないですね(逆だったら気分が良いのですが^^;
kawaguti

2020/11/26 07:02

アプリケーションの追加と削除でみたところCore2.1とあったんですがバッタ物かもしれませんね こちらで動作しないと言うことはcsprojファイルの書き方が悪いのかもしれません。 レコードももちろんですが、new() の見やすさで C# 9.0に変更したくてしょうがないです。ググってもあまり情報がなかったのとエラーで諦めていましたがもう一度検索してみます が、もし、もしお構い無ければcsprojの内容を教えて頂けたら幸いです C#8はまたググってみます ありがとうございました
kawaguti

2020/11/27 23:43 編集

あぁ、気づかないうちにサンプルまで・・・ 何から何までありがとうございます
kawaguti

2020/11/28 06:59

頂いたサンプル動きました! 環境はWindows7 + Core3.1 + C# 9.0です これで手間はかかりますが現状のアプリケーションをC#9.0に移行できそうです。 ここまでご指導くださったおかげです、ありがとうございました
guest

0

こんばんは。

Prismを利用されているとのことなので、MVVMパターンに従って開発しているということで宜しいでしょうか。
完全コンストラクタを遵守するとして私が作るとしたら、

  • Model…完全コンストラクタのクラス
  • ViewModel…DataGridにバインドし、書き換えられたらModelのクラスを更新するクラス

とすると思います。
多分これが一番綺麗にまとまります。

##サンプルコード
###Model (C#)

C#

1// 参照関連面倒なので構造体にしてあります。クラスに書き換える場合は参照渡しになるので注意して下さい。 2public struct NameIndexPair 3{ 4 // プロパティの名前は適当です 5 public string Name { get; } 6 public int Index { get; } 7 8 public NameIndexPair(string name, int index) 9 { 10 Name = name; 11 Index = index; 12 } 13} 14 15public class MainClass 16{ 17 public MainClass() 18 { 19 public List<NameIndexPair> NameIndexPairs { get; } = new List<NameIndexPair>() 20 { 21 new NameIndexPair("あああ", 123), 22 new NameIndexPair("いいい", 456), 23 }; 24 } 25}

###MainWindow (XAML)

XAML

1<Window …略…> 2 <StackPanel> 3 <DataGrid ItemsSource="{Binding Items}" /> 4 </StackPanel> 5</Window>

###MainWindow (C#)

C#

1public class MainWindow : Window 2{ 3 public MainWindow() 4 { 5 InitializeComponent(); 6 DataContext = new MainWindowViewModel(); 7 } 8}

###ViewModel

C#

1//using Prism.Mvvm; 2//using Prism.Commands; 3 4public class MainWindowViewModel : BindableBase 5{ 6 public MainClass M { get; } = new MainClass(); 7 public ObservableCollection<NameIndexPair> Items { get; } = new ObservableCollection<NameIndexPair>(); 8 9 public MainWindowViewModel() 10 { 11 M.NameIndexPairs.ForEach(p => Items.Add(p)); 12 Items.CollectionChanged += (sender, e) => 13 { 14 M.NameIndexPairs = new List(); 15 Items.ForEach(p => M.NameIndexPairs.Add(p)); 16 }; 17 } 18}

実行環境が手元に無いので動作確認出来ていません。ごめんなさい。
もし動かなかったら言って下さい。

投稿2020/11/23 07:34

編集2020/11/23 13:03
Automatic9045

総合スコア313

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kawaguti

2020/11/23 12:16

こんばんは。 Modelでは完全コンストラクタのクラスを作成して、ViewModelでは書き換え可能なクラスを定義し、コレクションとすることで、Modelから載せ替え先とする。それをDataGridでバインディングして利用するということでしょうか 確かにViewModel内でのみ書き換え可能な一時的なクラスを用意すれば不用意なデータの修正の可能性は下がりそうです ですが、一度読み込んだデータの変更を認めず、変更はクラスのコンストラクタのみとする完全コンストラクタパターンからは少し外れているような気もします これば普通だよと言われればそれまでですが ともあれご回答ありがとうございます
Automatic9045

2020/11/23 12:27

読み直しましたが、言葉不足で意味不明な回答になってましたね……すみません。 サンプルコードを追記しようと思います。
kawaguti

2020/11/24 01:58

ご回答ありがとうございます わかりやすいコードをありがとうございます ViewModelでの載せ替えが肝ですね やはりMVVMパターンでありながらも完全コンストラクタを利用する場合では積み替えする方法がわかりやすいようですね 雑な質問文にコードまで提示頂きまして感謝いたします
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問