- 投稿日:2020-09-12T19:54:00+09:00
ASP.NET Core Identityの自習記録(.NET FrameworkCore3.0) 1日目
この自習の目標
- MVC.NET Coreの認証無しテンプレートで作成されたWebページにASP.NET Core Identityで認証を追加する
- 保存領域として、Postgresを利用
- ユーザー情報にカスタム項目を追加する
- ユーザー情報にいろいろなRoleを追加する
- その他、ある程度業務で使うだろう範囲で、色々いじくる&使い方を覚える
こんな感じで行こうと思っています。
当方たいした知識は無いので間違っていたら突っ込んでください・・・1.最低限認証が実施できる物を作成する
1-1.素のMVC.NET Coreを作成する
※ここは説明いらないと思いますが、
VS2019で新規プロジェクト>ASP.NET Core Webアプリケーションを選択
次の画面の設定はこんな感じ
※基本的に認証付きで作成したテンプレートに近づけていく作業です。1-2.参照パッケージ
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.AspNetCore.Identity.UI
Microsoft.EntityFrameworkCore
Npgsql.EntityFrameworkCore.PostgreSQL
Microsoft.EntityFrameworkCore.Tools1-3.DBContext追加
TestApplicationDbContext.csを作成して、Identityのクラスを継承したEFクラスを作成
TestApplicationDbContext.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace WebTeqKensho.Data { public class TestApplicationDbContext : IdentityDbContext { public TestApplicationDbContext(DbContextOptions<TestApplicationDbContext> options) : base(options) { } } }1-4.ログインリンクの追加
_LoginPartial.cshtmlを作成して、ログイン用のリンクのテンプレを作成
_LoginPartial.cshtml@using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @inject UserManager<IdentityUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @User.Identity.Name!</a> </li> <li class="nav-item"> <form class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })"> <button type="submit" class="nav-link btn btn-link text-dark">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul>標準のテンプレートにログインテンプレの読み込みを追加(_Layout.cshtmlの20行目に追加)
_Layout.cshtml<partial name="_LoginPartial" />1-5.Startup.csの設定
ConfigureServicesに処理を追加
Startup.cspublic void ConfigureServices(IServiceCollection services) { //EntityFramework接続の設定 services.AddDbContext<TestApplicationDbContext>(options => options.UseNpgsql( Configuration.GetConnectionString("DefaultConnection"))); //Identityと接続を紐付ける services.AddDefaultIdentity<IdentityUser>() .AddEntityFrameworkStores<TestApplicationDbContext>(); services.AddControllersWithViews(); //既存 }Configureに処理を追加
Startup.csapp.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); //認証のために追加 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapRazorPages(); //認証の為のページ遷移設定の読み込み });1-6.接続情報をappsettings.jsonに追加
※接続情報は人それぞれなので適当に変えてください。
appsettings.json"ConnectionStrings": { "DefaultConnection": "Server=localhost;Port=5432;User Id=tester;Password=tester;Database=testDB" },1-7.マイグレーションをしてDBにテーブルを追加
アップデート用ファイル作成(パッケージマネージャコンソールで実行)
Add-Migration firstIdentity
実行
Update-Database -Verbose
以上で最低限identityを利用して認証するための下準備が完了。
次回からは、カスタマイズを中心に調べていきますよ。
- 投稿日:2020-09-12T19:51:51+09:00
EntityFramework Core 自分用メモ
デスクトップにテキストでおいてあったけどなくしそうだったのでメモ。
※参考にしないでください⓪DBマイグレ
初回
Enable-Migrationsアップデート用ファイル作成
Add-Migration 投稿名実行
Update-Database -VerboseEntity Framework Core アノテーション
①[NotMapped]:DBに追加しないModelのプロパティ
②主キー
単体の場合:[Key]
modelBuilder.Entity<モデル名>().HasKey(c => c.キー名);
複合キー :
modelBuilder.Entity<モデル名>().HasKey(c => new { c.キー名1, c.キー名2 );
③データの自動生成
[DatabaseGenerated(DatabaseGeneratedOption.None)]
自動生成しない(主キーなどを自分の思い通り設定したい場合便利?)
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
レコード追加時自動で割り振り
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
追加更新時に自動で割り振り④Not Null制約
[Required]
⑤最大長
[MaxLength(500)]
⑥楽観的排他制御
[ConcurrencyCheck]:指定したフィールドが楽観的排他チェックの対処対象となる
DbUpdateConcurrencyExceptionが発生
[Timestamp]:行が更新追加された際にタイムスタンプが保存される行
排他チェックの対象となる→DbUpdateConcurrencyExceptionが発生⑦シャドウ プロパティ
モデルでは定義されてないけど、モデルのプロパティとして利用出来るよ!ってやつ?
modelBuilder.Entity<クラス名>().Property("シャドウプロパティ名");利用方法
context.Entry(モデルが入った変数).Property("シャドウプロパティ名").CurrentValue = DateTime.Now;
var XXXX = context.XXXX.OrderBy(b => EF.Property(b, "シャドウプロパティ名"));
使い道が今一わからん・・・?⑧リレーションシップ
https://docs.microsoft.com/ja-jp/ef/core/modeling/relationships
ア.完全に定義されたリレーションシップsample.cspublic class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }イ.外部キー プロパティ
sample.cspublic class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }ウ.単一のナビゲーション プロパティ
sample.cspublic class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } }エ.データ注釈
①外部キーの名前が完全に定義されたリレーションシップのような命名に寄らない場合
[ForeignKey("外部キーの名前")]sample.cspublic class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogForeignKey { get; set; } [ForeignKey("BlogForeignKey")] public Blog Blog { get; set; } }②一つのモデルから同じモデルに二つのリンクが張られている場合 [InverseProperty("リンク先のモデルのプロパティ名")]sample.cspublic class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int AuthorUserId { get; set; } public User Author { get; set; } public int ContributorUserId { get; set; } public User Contributor { get; set; } } public class User { public string UserId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [InverseProperty("Author")] public List<Post> AuthoredPosts { get; set; } [InverseProperty("Contributor")] public List<Post> ContributedToPosts { get; set; } }オ.複合外部キー(Fluentを使うしかない?)
modelBuilder.Entity<多の方のモデル名>()
.HasOne(s => s.1の方のモデル名)
.WithMany(c => c.1の方のプロパティ名)
.HasForeignKey(s => new { s.多の方のプロパティ名, s.多の方のプロパティ名 });カ.多対多
sample.csclass MyContext : DbContext { public DbSet<Post> Posts { get; set; } public DbSet<Tag> Tags { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<PostTag>() .HasKey(t => new { t.PostId, t.TagId }); modelBuilder.Entity<PostTag>() .HasOne(pt => pt.Post) .WithMany(p => p.PostTags) .HasForeignKey(pt => pt.PostId); modelBuilder.Entity<PostTag>() .HasOne(pt => pt.Tag) .WithMany(t => t.PostTags) .HasForeignKey(pt => pt.TagId); } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public List<PostTag> PostTags { get; set; } } public class Tag { public string TagId { get; set; } public List<PostTag> PostTags { get; set; } } public class PostTag { public int PostId { get; set; } public Post Post { get; set; } public string TagId { get; set; } public Tag Tag { get; set; } }⑨INDEX
modelBuilder.Entity<クラス名>().HasIndex(b => b.Indexを張るプロパティ名); ユニークにしたい場合.IsUnique() 複数も可.HasIndex(p => new { p.プロパティ名1, p.プロパティ名2 });⑩自動プロパティ・バッキングフィールド
DBから展開する場合は、アクセサを使わず直接バッキングフィールドに書き込まれる(ようだ)
sample.cspublic class Blog { private string _url; //←コレの命名規則は_<camel-cased property name> とかm_<camel-cased property name> public int BlogId { get; set; } public string Url { get { return _url; } set { _url = value; } } }⑪値の変換
作例のEnumの名前の方で保存されるよってこった 使い道は・・・??
sample.cspublic class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }⑫所有エンティティ型
入れ子のように使える。実テーブルは一個
sample.cs[Owned] public class StreetAddress { public string Street { get; set; } public string City { get; set; } } public class Order { public int Id { get; set; } public StreetAddress ShippingAddress { get; set; } }OnModelCreatingに
sample.cs#region OwnsOne modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress); #endregion #region OwnsOneString modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress"); #endregion #region ColumnNames modelBuilder.Entity<Order>().OwnsOne( o => o.ShippingAddress, sa => { sa.Property(p => p.Street).HasColumnName("ShipsToStreet"); sa.Property(p => p.City).HasColumnName("ShipsToCity"); }); #endregion以降はDBへの制約
⑭テーブル マッピング
sample.cs[Table("blogs")] [Table("blogs", Schema = "blogging")] //スキーマの設定⑮nvarchar(max)列マッピング
sample.cs[Column("blog_id")]⑯データの種類
[Column(TypeName = "varchar(200)")] decimal(5, 2) nvarchar(max) DateTime
使える種類はDBによって異なる⑰主キー
[Key]
命名したい場合sample.csmodelBuilder.Entity<Blog>() .HasKey(b => b.BlogId) .HasName("PrimaryKey_BlogId");⑱既定のスキーマ
sample.csmodelBuilder.HasDefaultSchema("blogging");⑲計算列
DBに計算列は作られないが使い道が・・・?
sample.csclass MyContext : DbContext { public DbSet<Person> People { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .Property(p => p.DisplayName) .HasComputedColumnSql("[LastName] + ', ' + [FirstName]"); } } public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string DisplayName { get; set; } }⑳シーケンス
※特定のテーブルに関連付けはされない
sample.csmodelBuilder.HasSequence<int>("OrderNumbers");㉑既定値 初期値
sample.csclass MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Rating) .HasDefaultValue(3); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } }※SQL フラグメントを利用することも出来る。
sample.cs.HasDefaultValueSql("getdate()");㉒インデックス
sample.csmodelBuilder.Entity<Blog>() .HasIndex(b => b.Url) .HasName("Index_Url");㉓フィルター
sample.csmodelBuilder.Entity<Blog>() .HasIndex(b => b.Url) .HasFilter("[Url] IS NOT NULL");㉔外部キー制約
sample.csclass MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey(p => p.BlogId) .HasConstraintName("ForeignKey_Post_Blog"); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
- 投稿日:2020-09-12T18:15:11+09:00
[C#/WPF/MVVM]今さらMVVMについて調べた
はじめに
恥ずかしながら未だにWinForms案件ばかりでWPF、ましてやMVVMなんて使ったことがない時代遅れなプログラマーです。恥ずかしい・・・
そんな私の元にも遂にWPF案件が舞い降りてきてしまった。
ここ数日「WPFでどうやって作るねん」から調べ始めて、今さらMVVMという存在を知り、「MVVMってなんやねん」をずっと調べているけど・・・なんだかよく分からない!
「
View
-ViewModel
-Model
に分けて作りましょう」
「テストしやすくなるよ」
とかざっくりとは分かる。
でも具体的なその分け方が調べていても人によって微妙に違って何が正解か分からない。1
そもそも正解なんて無いのかもしれない。で、考え込みすぎて訳が分からなくなってきたので、自分なりの考えを整理するためにも、MVVMについて書いてみることに。
WinForms脳な人間がMVVMについて数日調べただけの内容なので間違えてる部分が多々あるかもしれません。その時は優しく教えてくれると嬉しいです。優しく・・・
なんでMVVMに分けるのだろう?
V-VM-M
の分け方について調べているだけだと明確な物が見えてこないので、視点を変えて
「なんでMVVMに分けるのだろう?」
という目的から考えてみることにした。目的
テスタビリティ(テスト容易性)向上
画面とコードがくっついてるとユニットテストがし辛い。
そこで画面(View)とコード(ViewModel/Model)に分離してやるとやりやすくなる。なるほど、たしかに。
プロダクティヴィティ(生産性)向上
画面(View)とコード(ViewModel/Model)が分離してると、デザイナは画面(View)、プログラマはコード(ViewModel/Model)と明確に分業できる。
もっともウチにはデザイナなんていない。
ポータビリティ(移植性)向上(?)
画面(View)とコード(ViewModel/Model)が分離してると、画面(View)を用意するだけで他のプラットフォームに対応できる・・・かもしれない。
と言うのも、これについて触れられている情報を殆ど見かけなくて、あくまで勝手なイメージ。
ただ理論上は可能なはず・・・2分け方が曖昧な原因はここにある?
恐らく大まかにこの3つが目的で、特にテスタビリティ向上が重要っぽい。
あれ?これらの目的って、とりあえず画面さえ分離しちゃえば一応は満たせる?
そのせいでV-VM-M
の分け方が曖昧なんだろうか。理想的なプロジェクト構成を考えてみる
↑に挙げた目的を元に、まずはどんなプロジェクト構成が理想的かを大まかに考えてみる。
こんな感じ?
- MvvmTest (.Net Core/WPF) - Views - MainView.xaml - MainView.xaml.cs - App.xaml - App.xaml.cs - AssemblyInfo.cs - MvvmTest.MVM (.Net Standard) - MainViewModel.cs - MainModel.csあくまで最小の基本構成。DIコンテナとか画面遷移とか考え出すと多分これじゃあ足りない。
MvvmTest プロジェクト
Viewとアプリケーションを定義・実装するプロジェクト。
ViewをMvvmTest.Views
プロジェクトに分けるのも有りかも。意味があるかは謎。
あとこれをMvvmTest.NetCore
にして、XamarinでMvvmTest.iOS
とか作れると幸せ。MvvmTest.MVM プロジェクト
ViewModel と Model を定義・実装するプロジェクト。
.Net Standard
にしたい!!3
MVM・・・もう少し良い名前はないものか。
MvvmTest.ViewModels
とMvvmTest.Models
に分けるのも有りかも。意味があるかは謎。
ViewModel と Model は同じ階層で近い位置にいた方が便利のような?MVVMそれぞれの役割を考えてみる
次に
View
、ViewModel
、Model
それぞれの役割を大まかに考えてみる。View
画面。それ以上でもそれ以下でもない。
ViewModel
が提供するプロパティに沿って画面を定義する。
デザインに関すること(XAML)以外は書きたくない。Model
ロジック。やるべき処理をやる人。
ViewModel
からパラメータを貰って処理を行う。
どんなデザインの画面かは知らない。4ViewModel
画面とロジックを結ぶ橋渡し役。
画面からどんな情報が欲しいか定義して、ロジックの求める形で情報を渡して、ロジックから結果を貰って、結果を画面に渡してあげる。
大体どんな画面か知っていて5、画面寄りの処理(入力チェックとか)はこの人がやるべき?(不明瞭)それぞれ持つべきものを考えてみる
↑の役割をふまえて、
View
、ViewModel
、Model
がそれぞれ持つべきもの(やるべきこと)を大まかに考えてみる。View
- 画面デザイン
- 対応する
ViewModel
のインスタンス(View
はViewModel
に依存、DataContext)- コントロールが
ViewModel
のどのプロパティと連動するか(Binding)ViewModel
- 対応する
Model
のインスタンス(ViewModel
はModel
に依存)- 画面の入力データを受け取る6
- 入力データが
Model
に渡せるかどうかの入力チェック7- エラーを
View
に通知(INotifyDataErrorInfo)- ボタンなどのイベント処理(ICommand)
- 入力データを
Model
の求める形に変換して渡すModel
のプロパティ変更通知を受け取る- プロパティ変更を
View
に通知(INotifyPropertyChanged)Model
ViewModel
からパラメータを受け取る- パラメータのエラーチェック
- エラーを
ViewModel
に通知(INotifyDataErrorInfo)- 実際にやりたい処理
- 結果をプロパティに格納
- プロパティ変更を
View
に通知(INotifyPropertyChanged)実際に書いてみる
今まで考えたことをふまえて、実際にコードを書いてみる。
数値を2つを渡して実行すると足し算した結果が返ってくる超単純なアプリをMVVMで作ってみた。Model(MainModel.cs)
まずは画面は気にせずやるべき処理を書いてみる。
class MainModel : ViewModelBase, IMainModel { private Property<int> ans = new Property<int>(); public int ParamA { get; set; } public int ParamB { get; set; } public int Answer { get => this.ans; set => this.ans.SetValue(value, this); } public void Sum() => this.Answer = this.ParamA + this.ParamB; }2つの数字を受け取って足し算するだけ。ややこしいのでパラメータエラーはなし。
因みにViewModelBase
クラスにプロパティ変更通知が実装されていて、Property<T>
のSetValue
で良い感じに通知してる。
IMainModel
は、MainModel
の定義そのまま。ViewModel(MainViewModel.cs)
次に画面を想像しつつ
Model
と繋ぐViewModel
を書いてみる。
数字を入力するテキストボックス2個(ParamA,ParamB)と、実行ボタン1個(SumCommand)と、結果を表示するラベルが1個(Answer)かな。public class MainViewModel : ViewModelBase { private IMainModel Model { get; } private Property<string> paramA = new Property<string>("0", (value) => { if (value.Length == 0) return "未入力エラー"; else if (!int.TryParse(value, out int _)) return "フォーマットエラー"; else return null; }); private Property<string> paramB = new Property<string>("0", (value) => { if (value.Length == 0) return "未入力エラー"; else if (!int.TryParse(value, out int _)) return "フォーマットエラー"; else return null; }); private Property<int> ans = new Property<int>(); public string ParamA { get => this.paramA; set { // 入力エラーがなければModelに設定 if (this.paramA.SetValue(value, this)) this.Model.ParamA = int.Parse(this.ParamA); } } public string ParamB { get => this.paramB; set { // 入力エラーがなければModelに設定 if (this.paramB.SetValue(value, this)) this.Model.ParamB = int.Parse(this.ParamB); } } public int Answer { get => this.ans; set => this.ans.SetValue(value, this); } public ICommand SumCommand { get; } public MainViewModel() { // TODO: 本来ならDIコンテナから取得 this.Model = new MainModel(); this.Model.PropertyChanged += Model_PropertyChanged; // Sumボタン this.SumCommand = new Command(() => { // 実行 this.Model.Sum(); }, paramA, paramB); } private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(this.Model.Answer)) this.Answer = this.Model.Answer; } }
View
からデータを受け取って、Model
に渡せるデータかチェックして、SumCommandの有効・無効を制御。
SumCommandの処理でModel
にパラメータを渡して実行。
Model
のプロパティ変更通知で結果を取り出してViewModel
に通知。独自のクラスが多くて分かりにくい・・・
ひとまずProperty<T>
には検証機能もあって、Command
クラスには指定したProperty<T>
のエラー状態と連動して有効・無効が勝手に切り替わる・・・くらいのイメージで・・・8View(MainView.xaml)
最後に画面。
ViewModel
に従って数字を入力するテキストボックス2個と、実行ボタン1個と、結果を表示するラベルが1個。<Window x:Class="MvvmTest.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MvvmTest.Views" xmlns:vm="clr-namespace:MvvmTest.VM;assembly=MvvmTest.VM" mc:Ignorable="d" Title="Main" SizeToContent="WidthAndHeight"> <Window.DataContext> <vm:MainViewModel /> </Window.DataContext> <Grid MinWidth="300" MinHeight="200"> <Grid.ColumnDefinitions> <ColumnDefinition Width="100" /> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <!-- ParamA --> <TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" Text="ParamA"/> <TextBox Grid.Row="0" Grid.Column="1" MaxHeight="24" Margin="10, 0, 10, 0" VerticalContentAlignment="Center" Text="{Binding ParamA, UpdateSourceTrigger=PropertyChanged}" /> <!-- ParamB --> <TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" Text="ParamB"/> <TextBox Grid.Row="1" Grid.Column="1" MaxHeight="24" Margin="10, 0, 10, 0" VerticalContentAlignment="Center" Text="{Binding ParamB, UpdateSourceTrigger=PropertyChanged}" /> <!-- Button --> <Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,10,10,10" Content="Sum" Command="{Binding SumCommand}"/> <!-- Answer --> <TextBlock Grid.Row="3" Grid.Column="0" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" Text="Answer"/> <TextBlock Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left" Margin="10, 0, 10, 0" VerticalAlignment="Center" Text="{Binding Ans}" /> </Grid> </Window>画面のデザインと、ViewModelが誰かと、コントロール毎のバインディングを定義するだけ。
コードビハインド(MainView.xaml.cs)
/// <summary> /// MainView.xaml の相互作用ロジック /// </summary> public partial class MainView : Window { public MainView() { InitializeComponent(); } }生成されたコードそのまま。絶対に触らない(鉄の意志)
ソースコードの全てはGitHubに上げてみた
mitsu-at3/MvvmTest: Mvvm Test Project
仕事でGitLabは使っているけど、GitHubは初めて使った・・・(今さら)合ってるだろうか?
色々と調べたりMVVMの目的を考えると、この分け方が1番しっくり来るけど、合ってるのか分からない・・・
特にViewModel
に定義するView
からデータを受け取るためのプロパティの型を、例えば数値入力でもテキストボックスならstring型みたいに、View
のコントロールの都合を考慮した型にするのが正しいのかどうかが謎。
調べても明確にそういう考え方してる情報に出会えなかった。ただ
ViewModel
のプロパティをintにしてしまうと、View
の段階で入力エラーが出てしまって、そのままだとViewModel
側でエラーを検知できないから、View
からViewModel
に伝える仕組みが必要になってしまう。
でもView
にはなるべく余計なものは書きたくない。
って考えると、ViewModel
側はとりあえずstringで受け入れて、入力チェックはViewModel
側でやるのがベター?9調べてる中で気になったこととか
最後に調べてるときに見かけた情報で「それ合ってるの?」と気になったことなど。
あちこちの情報をひたすら漁ってる中で見かけた情報なのでどこで見たのかは覚えてない・・・10Modelがただのデータクラス
public class PersonModel { public string Name { get; set; } public int Age { get; set; } }Modelの意味を勘違いしてるのか、ロジックはこれから実装するつもりだったのか・・・
「ViewModelとModelは同じプロパティが並びます」
必ずしも同じ必要は無いのではなかろうか?
同じだとViewModel
が架け橋する意味も薄くなっちゃう。
ViewModel
とModel
の関係が崩れなければ、違っていても良い気がする。ViewModelが完全にModelへの受け渡しだけ
↑と似ているけど・・・
public class PersonModelView { public string Name { get => this.Model.Name; set => this.Model.Name = value; } public int Age { get => this.Model.Age; set => this.Model.Age= value; } // ・・・Modelから通知を受けたり、コマンドを受けてModelを実行したりだけ }入力チェックも何もない画面なら有りなのかもしれないけど、ここまで来るとVMとMを分ける意味がないような・・・
超小規模なプロジェクトならModel
なしも有り?あなたのモデルはどこから?
public MainViewModel(IMainModel model) { this.Model = model; }なんやかんやの仕組みがあって自動的に依存性注入で渡ってくるならこれも良いと思う。
でもこれは嫌だ(MainView.xaml.cs)
/// <summary> /// MainView.xaml の相互作用ロジック /// </summary> public partial class MainView : Window { public MainView() { IMainModel model = DIコンテナ的なやつ.GetService(typeof(IMainModel)); this.DataContext = new MainViewModel(model); InitializeComponent(); } }コードビハインドには書きたくない!!!
ViewModelのコンストラクタで作れば良くない?
public MainViewModel() { this.Model = DIコンテナ的なやつ.GetService(typeof(IMainModel)); }
ViewModel
がModel
に依存するのは間違いじゃないので、素直にこっちの方が良いような。Modelがシングルトン
これが正しいのかが1番分からない。
今までの考えからすると、View
-ViewModel
-Model
は1つのセットと見なして、生存期間は同じにしておく方が自然のような気がする。
シングルトンで扱いたい機能は、Model
よりも下に定義して、Model
の中でそのシングルトンクラスを使った方がMVVMの関係性が明確で分かりやすくない?でも厳密には「
View
とViewModel
以外全てがModel
」らしいから11、シングルトンでも間違いではないのかな・・・
個人的には少なくともViewModel
が参照するModel
は、生存期間を同じにしておきたいなあ。おわりに
長々と書いていて「こう考えるのが正しいのではないか?」というのはところまでは来たけども、まだ確信を持って「この考えで正しい!」とは言えない・・・
しっくりは来てるんだけどなあ。
でも序盤に挙げた「目的」を明確に認識しておくだけでも「その形が間違ってるか?」の判断材料にはなりそう。
少しは最新の開発スタイルに近付けただろうか・・・?あと画面遷移とかダイアログ表示とかDIコンテナとか調べだすと、Prismとか外部フレームワークに頼りたくなる理由が分かってきた・・・
何となく自力でも実装できそうな雰囲気だけど、かなり面倒そうな印象。おまけ:DIコンテナって・・・
MVVMについて調べる中で初めて「DIコンテナ」を知ったのだけど(今さら)、解説を読んだときに真っ先にあいつを思い出した・・・
HRESULT CoCreateInstance( REFCLSID rclsid, LPUNKNOWN pUnkOuter, DWORD dwClsContext, REFIID riid, LPVOID *ppv );取得したいInterfaceの情報を渡すとインスタンスが返ってくるってこれじゃん。
そう思うと急に親しみが湧いてきたのでした。おしまい。
理解力が足りないだけかもしれない。 ↩
Xamarinを使えば出来たりしないかな?(Xamarin未経験) ↩
.Net Standard大好き ↩
何をする画面かは知ってる。じゃないと何の処理をすれば良いのか分からない。あくまでデザインを知らないだけ。 ↩
このパラメータはテキストボックス的なやつで、このパラメータはチェックボックス的なやつで・・・みたいな。 ↩
View
がバインディングするところでエラーが出ないようにするべき?(数値入力前提のテキストボックスでも、とりあえずstring型のプロパティで受け取るとか) ↩
Model
の処理に依存するエラーは知らない。あくまで「渡せるかどうか」だけ。 ↩
INotifyPropertyChanged
やINotifyDataErrorInfo
はどういう実装が便利だろう?とお試しで作ったクラス。実際にこれで良いかは分からない。(全容はGitHubで・・・) ↩stringにしておけばどんな入力でも受け入れられるテキストボックスだからってだけで、どんな型にしてもエラーが発生しうるコントロールの場合は、
View
→ViewModel
の通知は避けられない? ↩ただその情報のせいで余計に混乱した ↩
混乱の素 ↩
- 投稿日:2020-09-12T17:48:31+09:00
Prism の Prism Full App (.NET Core) テンプレートを見てみよう
Prism のプロジェクト テンプレートに少し前から Prism Full App (.NET Core) という名前のプロジェクト テンプレートが追加されています。
このプロジェクト テンプレートを使うと Prism を開発している人が、特に何も制約が無ければこんな風にプロジェクトをわけて作るといいんじゃないかという形になっていると思うので、これを見て構造を理解して自分がプロジェクトを作るときの参考にしてみましょう。
もし、この構成に異論がない場合は、この Prism Full App プロジェクト テンプレートを自分が開発するときの開始地点として使うのもいいですね。
では見ていきましょう。では早速 Prism Full App (.NET Core) をもとにプロジェクトを新規作成してみましょう。今回は WpfSampleFullApp という名前でプロジェクトを作りました。
このプロジェクトを作るだけで以下のように 6 個のプロジェクトが作られます。
WpfSampleFullApp をスタートアップ プロジェクトにして実行すると以下のように まさに Hello world のような見た目のアプリが起動します。
生成されたプロジェクト
では、1 つ 1 つプロジェクトを見ていきます。
WpfSampleFullApp プロジェクト
このプロジェクトがエントリーポイントになります。中身は非常にシンプルで App クラスと MainWindow と MainWindowViewModel があるだけです。
ここは特にいうところはないですね。WpfSampleFullApp プロジェクトが何を参照しているか見ていきましょう。以下のプロジェクトを参照しています。
- WpfSampleFullApp.Core
- WpfSampleFullApp.modules.ModuleName
- WpfSampleFullApp.Services
- WpfsampleFullApp.Services.Interfaces
端的に言うと全部って感じですね。では、この App クラスでどのようにモジュールを追加したり DI コンテナにクラスを登録しているか確認してみましょう。App.xaml.cs は以下のような感じになっています。
App.xaml.csusing Prism.Ioc; using WpfSampleFullApp.Views; using System.Windows; using Prism.Modularity; using WpfSampleFullApp.Modules.ModuleName; using WpfSampleFullApp.Services.Interfaces; using WpfSampleFullApp.Services; namespace WpfSampleFullApp { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterSingleton<IMessageService, MessageService>(); } protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule<ModuleNameModule>(); } } }最初に CreateShell メソッドを見てみます。これは特に説明するまでもないですが MainWindow がアプリケーションのシェルだよってことで単純に MainWindow のインスタンスを生成しています。
次に、ConfigureModuleCatalog メソッドを見てみましょう。単純に、今回のプロジェクトテンプレートで生成されている唯一のモジュールのプロジェクトを AddModule で追加していますね。
最後に RegisterTypes を見ていきます。
ここでは WpfSampleFullApp.Services.Interfaces で定義されたサービスのインターフェースと WpfSampleFullApp.Services で定義された実装クラスの紐づけですね。単純に DI コンテナに登録しています。ということでまとめると、この WpfSampleFullApp プロジェクトは、ざっくりと以下のことをしています。
- モジュールの登録
- サービスのインターフェースと実装の紐づけ
- メインの Window の作成
WpfSampleFullApp.Core プロジェクト
このプロジェクトはモジュールクラスで皆が使う共通的なクラスが定義されています。ほかのプロジェクトには依存しないプロジェクトです。そして以下のクラスが定義されています。
- RegionNames クラス
- Mvvm/ViewModelBase クラス
- Mvvm/RegionViewModelBase クラス
RegionNames クラス
これは単純に MainWindow に定義された Region の名前を定義した定数です。画面遷移するときなどに Region の名前を指定するのですがハードコーディングしないように、ここに定数として定義されていて、他のプロジェクトからはこれを参照するようになっています。
新たな Region を定義したら名前はここに定数として追加しましょう。Mvvm/ViewModelBase クラス
BindableBase を継承して IDestructible インターフェースを実装しているだけの、ほぼ空っぽのクラスです。
Mvvm/RegionViewModelBase クラス
これは名前の通り Region に設定する ViewModel の基本クラスです。
ViewModelBase を継承して INavigationAware, IConfirmNavigationRequest の 2 つのインターフェースを実装しています。Region に設定する ViewModel は画面遷移関係の処理を書くことが多いので画面遷移前と後のコールバックなどを提供する INavigationAware と、画面遷移前の確認処理を書く IConfirmNavigationRequest インターフェースは、よく実装するやつなので、ここで実装して空実装を提供している感じですね。
他にここに追加すると思われるもの
例えばダイアログとして表示する View の ViewModel の基本クラスなんかも追加することになるでしょうね。
あとは EventAggregator でやり取りする PubSubEvent の継承クラスなんかも、ここに追加することになるでしょう。WpfSampleFullApp.Services.Interfaces プロジェクト
ここにアプリケーションのモデル レイヤーのサービスが定義される感じです。依存しているプロジェクトはありません。もっというと Prism いも依存していません。
端的に言うと色々なモジュールの ViewModel レイヤーから使われるインターフェースの定義ですね。なのでモデル レイヤーが ViewModel に提供するインターフェースだけが定義される感じですね。もし自分のモデルレイヤーはユースケースというものを提供するんだ!みたいな感じだとしたら WpfSampleFullApp.UseCases.Interfaces みたいになるのかな。
後は、混ぜるか別のプロジェクトに定義するのか微妙なラインですが、モデル レイヤーがモデル レイヤー以外に提供するインターフェースを定義するプロジェクトにするなら、DB や WebAPI にアクセスするクラスが実装するための Repository 系のインターフェースもここに定義することになるでしょう。文脈としては GetAll とか GetById とか Update, Save のような DB からみた文脈のインターフェースではなく、あくまでモデルが外部リソースに求める文脈で名前が付けられたインターフェースが定義されるような場所ですね。デフォルトでは、何かメッセージを提供するだけの IMessageService インターフェースが定義されています。
WpfSampleFullApp.Services プロジェクト
ここに WpfSampleFullApp.Services.Interfaces プロジェクトで定義されたサービスのインターフェースの実装クラスが定義されます。これも Prism に依存していません。MessageService があるだけです。
参照しているプロジェクトは先ほどの WpfSampleFullApp.Services.Interfaces プロジェクトになります。
ここにモデル レイヤーのコア ロジック系が来る感じですね。WpfSampleFullApp.Modules.ModuleName プロジェクト
これがモジュールのプロジェクトで、View や ViewModel が定義されています。ModuleName という残念な感じのプロジェクト名になっているので、実際にはこいつは消して Prism Module (.NET Core) プロジェクト テンプレートをベースに作り直すことになるでしょう。
ポイントとしては、WpfSampleApp.Modules.Services.Interfaces プロジェクトに依存しているだけで WpfSampleApp.Modules.Services プロジェクトには依存していないところです。あくまで依存先はインターフェースで実装ではないという点がポイントです。
実装とインターフェースの紐づけを知っているのは WpfSampleFullApp プロジェクトだけになります。
まとめてみよう
ということでこんな感じの依存関係になっています。
Web API 呼んでみよう
Web API というほど大したものじゃないですけどメッセージを Web 経由でとってくるようにしてみたいと思います。
具体的には以下の URL から返される JSON の message プロパティを今表示されている Hello from the Message Service のかわりに表示するようにします。https://raw.githubusercontent.com/runceel/mockapi/master/message.json
返される JSON
message.json{ "message": "Hello from GitHub" }MessageService に直接 HttpClient 使った処理とか書いてもいいですが、テスト的にはいまいちなので外部リソースアクセスは Repository レイヤーを作りましょう。
WpfSampleFullApp.SErvices.Interfaces プロジェクトに以下のようなリポジトリ用インターフェースを定義します。Repositories/IMessageRepostory.csusing System.Threading.Tasks; namespace WpfSampleFullApp.Services.Interfaces.Repositories { public interface IMessageRepository { ValueTask<string> GetMessageAsync(); } }IMessageService も非同期になるので、あわせて変更しておきましょう。
IMessageService.csusing System.Threading.Tasks; namespace WpfSampleFullApp.Services.Interfaces { public interface IMessageService { ValueTask<string> GetMessageAsync(); } }モジュールのプロジェクトの ViewAViewModel クラスを書き換えて以下のようにしました。コンストラクタでデータ読み込みとかはあまりしないと思いますが、今回はもとがそうなってたので最小限のコードの変更でということで以下のようにしました。
ViewAViewModel.csusing WpfSampleFullApp.Core.Mvvm; using WpfSampleFullApp.Services.Interfaces; using Prism.Regions; using System.Threading.Tasks; namespace WpfSampleFullApp.Modules.ModuleName.ViewModels { public class ViewAViewModel : RegionViewModelBase { private string _message; public string Message { get { return _message; } set { SetProperty(ref _message, value); } } public ViewAViewModel(IRegionManager regionManager, IMessageService messageService) : base(regionManager) { Message = "Loading..."; messageService.GetMessageAsync().AsTask().ContinueWith(x => { Message = x.Result; // エラーはとりあえず考えない }); } public override void OnNavigatedTo(NavigationContext navigationContext) { //do something } } }そして WpfSampleFullApp.Repositories というプロジェクトを作って WpfSampleFullApp.Services.Interfaces プロジェクトへの参照を追加します。ここで先ほどの IMessageRepository の実装を行います。System.Text.Json への参照を追加してさくっと実装してしまいましょう。
MessageRepository.csusing System; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; using WpfSampleFullApp.Services.Interfaces.Repositories; namespace WpfSampleFullApp.Repositories { public class MessageRepository : IMessageRepository { private readonly HttpClient _httpClient; public MessageRepository(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public async ValueTask<string> GetMessageAsync() { using var jsonStream = await _httpClient.GetStreamAsync( "https://raw.githubusercontent.com/runceel/mockapi/master/message.json"); var result = await JsonSerializer.DeserializeAsync<MessageResult>(jsonStream, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, } ); return result.Message; } } public class MessageResult { public string Message { get; set; } } }App.xaml.cs でリポジトリ クラスの実装を DI コンテナに追加する処理を追加します。WpfSampleFullApp プロジェクトに WpfSampleFullApp.Repositories プロジェクトへの参照を追加するのも忘れないようにしましょう。
App.xaml.csusing Prism.Ioc; using WpfSampleFullApp.Views; using System.Windows; using Prism.Modularity; using WpfSampleFullApp.Modules.ModuleName; using WpfSampleFullApp.Services.Interfaces; using WpfSampleFullApp.Services; using System.Net.Http; using WpfSampleFullApp.Services.Interfaces.Repositories; using WpfSampleFullApp.Repositories; namespace WpfSampleFullApp { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App { protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 以下 2 行を追加 containerRegistry.RegisterSingleton<HttpClient>(); containerRegistry.RegisterSingleton<IMessageRepository, MessageRepository>(); containerRegistry.RegisterSingleton<IMessageService, MessageService>(); } protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { moduleCatalog.AddModule<ModuleNameModule>(); } } }ここまで出来たら実行してみましょう。以下のように表示されるはずです。
いい感じですね。プロジェクトの依存関係的には以下のようになりました。
単体テスト
単体テストプロジェクトは、デフォルトだとモジュール向けのものが作られています。Services 系のプロジェクトもテストする場合は必要に応じてモデル レイヤー向けの単体テスト プロジェクトを追加するといいでしょう。
まとめ
Prism Full App (.NET Core) プロジェクトテンプレートを眺めてみて、ちょっとだけ処理追加したりしてみましたが、こいつをベースに色々追加していくのは個人的にはアリかなと思いました。
プロジェクト テンプレートが吐き出すコードにちょっと処理追加しただけですが、一応今回のコードは GitHub にあげてます。
- 投稿日:2020-09-12T05:19:55+09:00
Unityエディタ拡張 Drag & Drop 領域をちょっと便利にする ( 備忘録 )
個人的に便利なDrag & Dropのエディタ拡張をメモ
完成図
完成系はこんな感じです、テスト用にターゲットとする拡張子を変更できるようにしてます。
記事用動画 pic.twitter.com/4FRBhqSanw
— 松本隆介 (@matsumotokaka11) September 11, 2020スクリプト
using System.IO; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Events; using UnityEditor; namespace Sample { public class DragAndDropEditor : EditorWindow { [MenuItem("Custom/DragAndDropEditor")] static void ShowWindow() { var window = GetWindow<DragAndDropEditor>(); window.titleContent = new GUIContent("DragAndDropEditor"); } // テスト用. StringBuilder stringBuilder = new StringBuilder(512); string targetExtension = ".unity"; // UI描画. void OnGUI() { // テスト用,ターゲットの拡張子を変更する. targetExtension = EditorGUILayout.TextField("targetExtension", targetExtension); // 目的とするファイルの拡張子を指定してDrag & Dropの領域を描画する. // 複数ファイルドロップに対応し、指定した拡張子以外のファイルがマスクされた配列がコールバックで呼ばれる. DrawFileDragArea( GUILayoutUtility.GetRect(0.0f, 75.0f, GUILayout.ExpandWidth(true)), "Drag & Drop", targetExtension, (objs) => { stringBuilder.Clear(); foreach (var obj in objs) { stringBuilder.AppendLine(obj.name); } }); if (GUILayout.Button("Clear")) { stringBuilder.Clear(); } // 取得したアイテムのリストを描画. EditorGUILayout.LabelField(stringBuilder.ToString(), GUILayout.Height(300)); } /// <summary> /// ドラッグアンドドロップ領域を描画,コールバックで取得したオブジェクトを得る. /// </summary> /// <param name="dropArea"> Drag & Dropを受け付ける範囲. </param> /// <param name="dropAreaMessage"> Drag & Dropを受け付ける領域に描画する文字列. </param> /// <param name="targetFileExtension"> 目的のファイルの拡張子. </param> /// <param name="dropCallback"> Dropされた時のコールバック. </param> /// <param name="visualMode"> マウスカーソルの見ため,基本的にGenericでよいと思われる. </param> private void DrawFileDragArea( Rect dropArea, string dropAreaMessage, string targetFileExtension, UnityAction<UnityEngine.Object[]> dropCallback, DragAndDropVisualMode visualMode = DragAndDropVisualMode.Generic) { Event evt = Event.current; GUI.Box(dropArea, dropAreaMessage); switch (evt.type) { // ドラッグ中. case EventType.DragUpdated: case EventType.DragPerform: if (!dropArea.Contains(evt.mousePosition)) break; // Dragされている間のカーソルの見た目を変更. DragAndDrop.visualMode = visualMode; if (evt.type == EventType.DragPerform) { // オブジェクトを受け入れる. DragAndDrop.AcceptDrag(); if (0 < DragAndDrop.objectReferences.Length) { var dropObjects = new List<UnityEngine.Object>(); for (var i = 0; i < DragAndDrop.paths.Length; ++i) { if (Path.GetExtension(DragAndDrop.paths[i]) == targetFileExtension) dropObjects.Add(DragAndDrop.objectReferences[i]); } if (0 < dropObjects.Count) dropCallback?.Invoke(dropObjects.ToArray()); } DragAndDrop.activeControlID = 0; } Event.current.Use(); break; } } } }