在之前标题正文的基础上,添加
DDD套路,推荐“三部曲两头挤”:
public class Article : BaseEntity { public int? InCategoryId { get; set; } public Category InCategory { get; set; }
public class Category : BaseEntity { //public IList<Article> Articles { get; set; } 可选 public string Name { get; set; }
<label>分类:</label> @Html.DropDownListFor(m=>m.SelectedCategoryId, new SelectList(Model.CategoryOptions,"Id", "Name"))
public class NewModel { public int SelectedCategoryId { get; set; } public IList<CategoryModel> CategoryOptions { get; set; } } public class CategoryModel { public int Id { get; set; } public string Name { get; set; }
public ActionResult New() { NewModel model = new NewModel { //为测试UI效果临时实现 CategoryOptions = new List<CategoryModel> { new CategoryModel { Id = 1, Name = "C#"} } }; return View(model);之后引入Service(Mock)实现
CategoryOptions = service.GetCategories()
public class ArticleService : IArticleService { public IList<CategoryModel> GetCategories() { return new List<CategoryModel> { new CategoryModel { Id = 1, Name = "C#"}, new CategoryModel { Id = 2, Name = "SQL"}
ProdService的实现,依赖于:
public IList<CategoryModel> GetCategories() { IList<Category> categories = categoryRepository.GetBy(GetCurrentUser()); return mapper.Map<IList<CategoryModel>>(categories);
不要忘了BaseService中配置映射:
cfg.CreateMap<Category, VM.Article.CategoryModel>();注意这里只能使用Id进行比较:
public IList<Category> GetBy(User user) { return dbSet.Where(c => c.Owner.Id == user.Id).ToList();
演示:没有category选项
@想一想@:一开始用户没有分类怎么办?
我们选择实现方案1
if (categories.Count == 0) //ToList()之后的categories不会为null { Category defaultCategory = new Category { Name = "默认分类", Owner = current }; categoryRepository.Save(defaultCategory); //不要忘了这一步,^_^ categories.Add(defaultCategory); }//else nothing
在ArticleService的Publish()中带上分类。
现在可以使用AutoMapper了
Article article = mapper.Map<Article>(model);
Author无法自动映射,需要手动赋值:
article.Author = GetCurrentUser();
但注意Model和entity中属性名有差异,需要自定义映射:
cfg.CreateMap<NewModel, Article>() .ForMember( a => a.InCategoryId, opt => opt.MapFrom(n => n.SelectedCategoryId))
复习:如果需要“纯面向对象”抛弃InCategoryId只保留InCategory的话,需要使用:
article.InCategory = categoryRepository.Load(model.SelectedCategoryId);
Keyword和Article多对多的entity声明和映射:略
View和Model:
@Html.TextBoxFor(m => m.Keywords)
public string Keywords { get; set; }这里就出现了和entity中Article的Keywords不匹配的问题:
public class Article : BaseEntity { public List<Keyword> Keywords { get; set; }
用户输入的是带有空格的字符串,但我们后台需要的是entity集合。所以,在ArticleService.Publish()中:
//@想一想@:为什么要Trim() string[] keywords = model.Keywords.Trim().Split(' '); for (int i = 0; i < keywords.Length; i++) { //@想一想@:为什么要这个判断? if (!string.IsNullOrWhiteSpace(keywords[i])) { Keyword keyword = new Keyword { Name = keywords[i] }; //复习:??运算符的作用 article.Keywords = article.Keywords ?? new List<Keyword>(); article.Keywords.Add(keyword);
不要忘了:用户输入的关键字有可能是重复的,这时候我们不能再new了哟!
所以需要利用KeywordRepository检查
public Keyword GetByName(string name) { return dbSet.Where(k => k.Name == name).SingleOrDefault();
但凡涉及到查询,就要想到索引!
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Keyword>() //不然默认nvarchar(max)不能生成索引 .Property(k => k.Name).HasMaxLength(256); modelBuilder.Entity<Keyword>() .HasIndex(k => k.Name) .IsUnique() //索引unique
最后的代码:
Keyword keyword = keywordRepository.GetByName(keywords[i]); if (keyword == null) { keyword = new Keyword { Name = keywords[i] }; }@想一想@:这样就OK了么?
标题和正文已经完成(略),关键在
比如:Author、Category和Keywords
所以首先要把这些关联对象显式的加载出来:
public Article GetWhole(int id) //命名太丑陋了,(*/ω\*) { return dbSet.Where(a => a.Id == id) .Include(a => a.Author) .Include(a => a.InCategory) .Include(a => a.Keywords) .SingleOrDefault();
相应的,在SingleModel中添加关联Model:
public class SingleModel { public _UserInfoModel Author { get; set; } public CategoryModel InCategory { get; set; } public IList<KeywordModel> Keywords { get; set; }
KeywordModel的映射需要AutoMapper配置:
cfg.CreateMap<Keyword, VM.Article.KeywordModel>();
最后,页面显示:
<a href="/User/@Model.Author.Id">@Model.Author.UserName</a>
<span>分类:</span> <a href="/Article/Category/@Model.InCategory.Id">@Model.InCategory.Name</a>
<span>关键字:</span> @foreach (var item in Model.Keywords) { <a href="/Article/Keyword/@item.Id">@item.Name</a> }
在文章单页添加一个编辑链接:/Article/Edit/{id}
@想一想@:文章Id从何而来?
<a href="/Article/Edit/@ViewContext.RouteData.Values["id"]">修改</a>
@想一想@:文章发布,和文章编辑,是否可共用:一个View/Model/Action?
除Action以外,其他都暂时是可以的,\(^o^)/
public ActionResult Edit(int id) { return View("New"); } [HttpPost] public ActionResult Edit(NewModel model, int id)
进入编辑页面,首先要加载被编辑文章内容:
NewModel model = service.GetEdit(id);
public NewModel GetEdit(int id)类似于SingleModel Get(int id),首先获取article(entity):
Article article = articleRepository.GetWhole(id);但是,
NewModel model = mapper.Map<NewModel>(article);所以不要忘了添加Map:
cfg.CreateMap<NewModel, Article>() .ReverseMap() //简化代码 //但ignore的配置还是不能自动双向 .ForMember(n => n.Keywords, opt => opt.Ignore()) ;
model.Keywords = string.Join(" ", article.Keywords.Select(k=>k.Name).ToArray() );
只能用所有POST内容“覆盖”原article内容,不管某项内容(比如Title/Category)用户实际上有无修改……@想一想@:为什么只能这样?
在Post的Action方法里面直接调用:
service.Edit(model, id);
对比之前的Publish()方法:
Article article = articleRepository.Find(id);
mapper.Map(model, article);
抽象之前的关键字转化代码,封装成方法transferKeywords(),然后调用:
transferKeywords(model.Keywords, article);
演示:抛出重复异常,
Violation of PRIMARY KEY constraint 'PK_dbo.KeywordArticles'
原因:复习多对多关系更改
解决方案,推荐引入LazyLoad,然后“先清场,后添加”:
article.Keywords.Clear();@想一想@:这样改了之后,transferKeywords()还能不能被Publish()和Edit()重用?
我们使用“静态消息”和“积分明细记录”机制。
因为使用(现阶段版本的)EF往集合中添加元素,会导致所有已有集合元素被加载,而User的Message和Credit可能会超级多,所以我们演示在Service层实现:
PS:以上不懂就复习:消息和积分的架构
Message message = new Message { Body = $"{user.UserName}使用了你的邀请码完成注册", Receiver = invitedBy, Kind = Kind.InvitedBy }; messageRepository.Add(message);
Credit credit = new Credit { Owner = invitedBy, Amount = 10, Comment = $"{user.UserName}使用了你的邀请码完成注册", Balance = creditRepository.GetLatest()?.Balance ?? 0 + 10 }; creditRepository.Add(credit);
注意:
protected UserRepository userRepository; protected MessageRepository messageRepository; protected CreditRepository creditRepository; public BaseService() { userRepository = new UserRepository(currentDbContext);
public Credit GetLatest() { return dbSet.OrderByDescending(x => x.Id).FirstOrDefault();
演示:列表页
//能不能foreach,为什么? for (int i = 0; i < Model.Items.Count; i++) { int fontWeight = Model.Items[i].ReadTime.HasValue ? 400 : 800; <div style="font-weight:@fontWeight;" > @*这个HiddenFor干嘛的?*@ @Html.HiddenFor(m => Model.Items[i].Id) @Html.CheckBoxFor(m => Model.Items[i].Selected) @Model.Items[i].Body#体会#:利用fontWeight控制样式……
在POST的Action中:
public ActionResult Index(IndexModel model) { foreach (var item in model.Items) { if (item.Selected) //checkbox勾选 { //item.Id:hidden input传回 messageService.Read(item.Id);
两种方案:
<button type="submit" formaction="/Message/Read">已读</button> <button type="submit" formaction="/Message/Delete">删除</button>对应:
public ActionResult Read(IndexModel model){ public ActionResult Delete(IndexModel model){缺点:不利于代码重用
<input type="submit" name="submit" value="已读" /> <input type="submit" name="submit" value="删除" />在后台通过value区分究竟是哪一个按钮被点中:
string btnValue = Request.Form["submit"];于是就可以重用代码:
if (btnValue == "已读")//这种字符串的比较总是怪怪的,^_^ { messageService.Read(item.Id); } else if (btnValue == "删除") { //因为“静态”消息,所以不用标记删除,就可以直接删除 messageService.Delete(item.Id); } else { //严谨做法:不是nothing #体会# throw new Exception("前台传回非标准数据……"); }
另:Delete()的实现可以不用Find(),稍稍优化:(复习)
public void Delete(int it) { T entity = new T { Id = it }; dbSet.Attach(entity); dbSet.Remove(entity);
为了给message的body套上HTML标签(听不懂还是复习,^_^)
在Global中添加HtmlTemplates文件夹,以及MessageTemplate,为“被作为邀请人注册”使用的方法Register.Invited()
namespace Global.HtmlTemplates { public static class MessageTemplate { public static class Register { public static string Invited(int id, string name) { return $@"<a href='/User/{id}'>{name}</a> 使用了你的邀请码完成注册";
其中,生成链接的功能还可以被进一步封装到SharedTemplate.Href:
public static string Common(string url, string text) { return $@"<a href='{url}'>{text}</a>";所以,最终的代码:
string url = $"/User/{id}"; return $@"{SharedTemplate.Href.Common(url, name)}使用了你的邀请码完成注册";
在生成message的时候调用:
Body = Global.HtmlTemplates.MessageTemplate .Register.Invited(user.Id, user.UserName),
复习:为什么需要?
我们可以直接创建一个控制台项目DbFactory,添加对
的引用。
我们首先需要拿到Repository中的DbContext,在Program.Main()中生成数据库和表结构:
SqlContext context = new SqlContext(); //因为LocalDB经常因为“被占用”无法删除,可以考虑总是手动删库 //context.Database.Delete(); context.Database.Create();
不要忘了在当前项目的App.config复制粘贴17bang的connectionStrings
准备好和Entity(或Controller)相对应的类,比如OnUser/OnArticle,每个类都有一个create()方法,里面依次生成一个一个的注册用户/文章等。
可以把生成并持久化entity的方法抽象出来,比如:
internal static void create() { fg = register("飞哥", null); atai = register("atai", fg); } private static User register(string name, User invitedBy) { User user = new User { UserName = name, Password = password, InvitedBy = invitedBy };
说明:
internal static User fg, atai;以供其他factory类使用,比如OnArticle时就需要作者
internal class OnArticle { internal static void create() { aspnet = publish("ASP.NET实战", "", OnUser.fg);
private static readonly string password = "1234";
专门建一个类:
internal class DbContextProvider { private static readonly SqlContext context = new SqlContext(); internal static SqlContext GetContext() { return context;
确保所有的操作在同一个DbContext中完成!(复习:避免DbContext冲突)
DbContextProvider.GetContext().Users.Add(user); DbContextProvider.GetContext().SaveChanges(); //也可以:new UserRepository(DbContextProvider.GetContext()).Add(fg);
千万不要仅仅把DbFactory当做一个只存储entity本身的工具!
DbFactory中的数据应该是和entity的业务逻辑一致的,比如用户是通过Register生成的,Register()的过程中还:
一种解决方案是为User添加一个方法:
public Message GetMessageOnRegister() { return new Message { Body = Global.HtmlTemplates.MessageTemplate .Register.Invited(Id, UserName), Receiver = InvitedBy, Kind = Kind.InvitedByr
然后,这个方法返回的Message就既可以在Service,也可以在DbFactory中使用!\(^o^)/
Message message = user.GetMessageOnRegister(); if (message.Receiver != null) //或者:if(message!=null) { DbContextProvider.GetContext().Messages.Add(message); DbContextProvider.GetContext().SaveChanges();
演示:成功生成User和Message……
完成以下功能:
使用dbFactory创建数据,并完成:
多快好省!前端后端,线上线下,名师精讲
更多了解 加: