使用 MVC 5 的 MessageBoardApp





5.00/5 (16投票s)
简单的消息/回复应用程序,使用 MVC 5
引言
网站内的消息传递在用户之间沟通想法方面起着重要作用。在页面内,可能有一些地方用户需要就某些事项与其他用户进行咨询。与其发送电子邮件或打电话,不如添加一个消息系统,在那里可以发布消息,并针对与关注的主题相关的问题发送回复。
背景
在本文中,我将解释一个简单的MessageBoard App,您可以在其中发布/创建消息,并为这些消息发送回复。我使用了SendGrid SMTP 客户端来向消息所有者发送电子邮件,以便在收到回复时通知他们。
我使用Visual Studio 2013中的MVC 5创建了演示应用程序,其中我只使用了两个视图 - 一个用于显示消息,另一个用于创建消息。我使用了PagedList来实现消息的显示分页,以显示固定数量的行。要将分页用于视图,我参考了以下两个关于在MVC中使用PagedList的文档:
http://stackoverflow.com/questions/25125329/using-a-pagedlist-with-a-viewmodel-asp-net-mvc
在上面的第一个参考链接中,PageList已被用于单个模型,而第二个参考链接提供了在ViewModel中使用PageList的解决方案,其中组合了多个模型,并可以在ViewModel中为特定模型进行分页。
描述
MessageBoard App要求用户登录才能创建消息或回复消息。当用户第一次登录应用程序时,如果已经有一些消息由其他用户创建,则应用程序将如下所示。
在下面,左侧是带有回复消息的主页。右侧的视图是用于创建消息的。如果用户从主页视图点击Post New Message链接,用户将被重定向到Create页面,并在用户提交页面后,将用户重定向回主页,其中提交的消息已列在消息列表中。
现在让我们看看如何从头开始创建应用程序并运行它。
创建项目
我在Visual Studion 2013中创建了一个全新的MVC 5 Web 项目,并将其命名为MessageBoard App。项目创建完成后,它在Solution Explorer中看起来如下。
设置 MessageBoardApp 的用户
ASPNET Identity 2.0 与 MVC 5 在数据库中不使用用户的全名。在我们的消息系统中,我们将使用用户的全名来轻松识别创建消息和发送消息回复的用户。
为此,我创建了一个ApplicationBaseController类,并更新了.LoginPartial.cshtml,以便它能够显示完整的用户名而不是默认的用户电子邮件。
我有一个关于这个的 CodeProject 技巧,地址是: CodeProject : https://codeproject.org.cn/Tips/991663/Displaying-User-Full-Name-instead-of-User-Email-in
对于我们的应用程序,ApplicationBaseController看起来如下。
using MessageBoardApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace MessageBoardApp.Controllers { public abstract class ApplicationBaseController : Controller { protected override void OnActionExecuted(ActionExecutedContext filterContext) { if (User != null) { var context = new ApplicationDbContext(); var username = User.Identity.Name; if (!string.IsNullOrEmpty(username)) { var user = context.Users.SingleOrDefault(u => u.UserName == username); string fullName = string.Concat(new string[] { user.FirstName, " ", user.LastName }); ViewData.Add("FullName", fullName); } } base.OnActionExecuted(filterContext); } public ApplicationBaseController() { } } }
完成此操作后,我们现在可以为我们的Messageboard App创建模型和视图模型。
创建模型
我在Models文件夹中像往常一样创建了两个模型:Message和Reply。Message类看起来如下。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MessageBoardApp.Models { public class Message { [Key] public int Id { get; set; } [Required] public string Subject { get; set; } [Required] public string MessageToPost { get; set; } public string From { get; set; } public DateTime DatePosted { get; set; } } }
而Reply类则如下所示。
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MessageBoardApp.Models { public class Reply { [Key] public int Id { get; set; } public int MessageId { get; set; } public string ReplyFrom { get; set; } [Required] public string ReplyMessage { get; set; } public DateTime ReplyDateTime { get; set; } } }
到目前为止,类定义中没有什么特别之处。
创建视图模型
我创建了一个新的ViewModels文件夹和一个名为MessageReplyViewModel的新类,该类具有以下方法和属性,如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MessageBoardApp.Models; using PagedList; using PagedList.Mvc; namespace MessageBoardApp.ViewModels { public class MessageReplyViewModel { private List<MessageReply> _replies = new List<MessageReply>(); public Reply Reply { get; set; } public Message Message {get;set;} public List<MessageReply> Replies { get { return _replies; } set { _replies = value; } } public PagedList.IPagedList<Message> Messages { get; set; } public class MessageReply { public int Id { get; set; } public int MessageId { get; set; } public string MessageDetails { get; set; } public string ReplyFrom { get; set; } public string ReplyMessage { get; set; } public DateTime ReplyDateTime { get; set; } } } }
请注意,我已使用
public PagedList.IPagedList<Message> Messages { get; set; }
用于MessageReplyViewModel中的Messages,而对于Replies,我没有使用PagedList,尽管两者都将在同一个视图中使用。我将在控制器操作和视图中解释如何使用分页。
创建视图
正如我之前所说,我将只使用两个视图,一个用于显示消息,用户可以在同一视图中回复消息,另一个用于创建新消息。第一个显示消息的视图,我在 ViewModel 中使用了 PagedList,其中一个模型使用了 PagedList,但另一个模型没有。
要在视图中使用PagedList,我们需要在视图页面的顶部定义以下内容。
@model MessageBoardApp.ViewModels.MessageReplyViewModel @using PagedList; @using PagedList.Mvc; <link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />
之后,我们需要在从中传递数据的控制器中设置页面大小和页码。
让我们来看看控制器方法。
public ActionResult Index(int? Id, int? page) { int pageSize = 5; int pageNumber = (page ?? 1); MessageReplyViewModel vm = new MessageReplyViewModel(); var count = dbContext.Messages.Count(); decimal totalPages = count / (decimal)pageSize; ViewBag.TotalPages = Math.Ceiling(totalPages); vm.Messages = dbContext.Messages .OrderBy(x => x.DatePosted).ToPagedList(pageNumber, pageSize); ViewBag.MessagesInOnePage = vm.Messages; ViewBag.PageNumber = pageNumber; --code block skipped for brevity }
我们可以看到上面的ActionResult有一些关于视图的页面大小和页码的设置。它还计算了数据库中已有的总消息数。设置好这些之后,我们就可以在视图中调用它们,如下所示。
Page @ViewBag.PageNumber of @ViewBag.TotalPages @Html.PagedListPager((IPagedList)ViewBag.MessagesInOnePage, page => Url.Action("Index", new { page }))
为了清楚起见,下面是从主页(消息列表)的输出
已在Index.cshtml中的<div>中定义,如下所示。
--code above <div class="form-group"> @using (Html.BeginForm("DeleteMessage", "Message", FormMethod.Post, new { @id = "form-message-delete", @class = "form-horizontal container" })) { <div class="col-sm-12"> <!-- table --> <table id="table-message-reply" class="table table-condensed table-striped table-message-reply"> <thead> <tr> <th class="tbl-subject">Subject</th> <th class="tbl-from">From</th> <th class="tbl-date">Date Posted</th> <th></th> <th></th> </tr> </thead> @foreach (var m in Model.Messages) { string selectedRow = ""; if (m.Id == ViewBag.MessageId) { selectedRow = "success"; } <tr class="@selectedRow"> <td> <div class="text">@m.Subject</div> </td> <td> <div class="text">@m.From</div> </td> <td> <div class="text">@m.DatePosted.ToString("dd/MM/yyyy")</div> </td> <td> @Html.ActionLink("View Reply", "Index", new { Id = m.Id }) </td> <td> <div class="text edit"> <a class="delete" href="#" title="delete" onclick="messageDelete(@m.Id)"><img style="width: 17px; height: 15px" src="~/Images/no.png" /></a> </div> </td> <td><input type="hidden" id="messageId" name="messageId" value="@m.Id"></td> </tr> } </table> Page @ViewBag.PageNumber of @ViewBag.TotalPages @Html.PagedListPager((IPagedList)ViewBag.MessagesInOnePage, page => Url.Action("Index", new { page })) <!-- category table end--> </div> } </div> --code below
Replies for Messages(消息回复)面板包含两部分:一部分用于Reply(回复)文本框、Reply(回复)按钮以及根据顶部面板中某个消息选择的View Reply(查看回复)而显示的Message Detials(消息详情)。
另一部分包含现有消息回复以及每条回复的详细信息。
在控制器方法中,我已如下定义了回复的视图。
--code removed for brevity if (Id != null) { var replies = dbContext.Replies.Where(x => x.MessageId == Id.Value).OrderByDescending(x => x.ReplyDateTime).ToList(); if (replies != null) { foreach (var rep in replies) { MessageReplyViewModel.MessageReply reply = new MessageReplyViewModel.MessageReply(); reply.MessageId = rep.MessageId; reply.Id = rep.Id; reply.ReplyMessage = rep.ReplyMessage; reply.ReplyDateTime = rep.ReplyDateTime; reply.MessageDetails = dbContext.Messages.Where(x => x.Id == rep.MessageId).Select(s => s.MessageToPost).FirstOrDefault(); reply.ReplyFrom = rep.ReplyFrom; vm.Replies.Add(reply); } } else { vm.Replies.Add(null); } ViewBag.MessageId = Id.Value; } --code removed for brevity
要使用控制器方法渲染视图,以下代码即可满足要求。
--code above @if (Model.Replies != null && ViewBag.MessageId != null) { <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> Replies for Message </h3> </div> <div class="panel-body"> <div class="form-horizontal container"> <div class="form-column col-lg-12 col-md-12 col-sm-12"> <div class="form-group"> <div class="col-sm-12"> <table class="table"> <tr> <td> <div class="form-group"> <span><b>Message Details: </b></span> @foreach (var item in Model.Replies) { if (item.MessageId == ViewBag.MessageId) { @item.MessageDetails } } </div> </td> </tr> <tr> <div class="form-group"> @using (Html.BeginForm("ReplyMessage", "Message", new { id = "form-reply-message", messageId = @ViewBag.MessageId }, FormMethod.Post)) { if (!ViewData.ModelState.IsValid) { <div class="row"> <div class="col-lg-4 col-md-4 col-sm-4"></div> <div class="col-lg-8 col-md-8 col-sm-8"> @Html.ValidationSummary(true) </div> </div> } @Html.HiddenFor(model => model.Reply.MessageId); <label class="col-sm-2 ">Reply</label> <div class="col-sm-9"> @Html.TextAreaFor(p => p.Reply.ReplyMessage, new { @rows = 2, @class = "form-control" }) @Html.ValidationMessageFor(model => model.Reply.ReplyMessage) </div> <div class="col-sm-1"> <input type="submit" class="btn btn-primary btn-success" value="Reply" id="btn-reply-message"> </div> } </div> </tr> </table> <h4>Replies for the Message</h4> <table class="table"> @foreach (var item in Model.Replies) { if (item.MessageId == ViewBag.MessageId) { <tr> <td> <div> <span><b>Reply Message : </b></span> @item.ReplyMessage </div> <div> <span><b>Reply From : </b> </span> @item.ReplyFrom </div> <div> <span> <b>Reply Date : </b> </span> @item.ReplyDateTime </div> </td> </tr> } } </table> </div> </div> </div> </div> </div> </div> <!-- start panel--> } --code below
我在页面上添加了更多功能,用于删除消息。
<td> <div class="text"> <a class="delete" href="#" title="delete" onclick="messageDelete(@m.Id)"><img style="width: 17px; height: 15px" src="~/Images/no.png" /></a> </div> </td> <td><input type="hidden" id="messageId" name="messageId" value="@m.Id"></td>
用于删除记录的 jQuery 方法,最终会调用控制器操作,如下所示。
<script> function messageDelete(index) { bootbox.dialog({ message: "Are you sure you want to delete the message ?", title: "Delete Message Confirmation", buttons: { success: { label: "Continue", className: "btn-success", callback: function deletemember() { $('#messageId').val(index); $('form#form-message-delete').submit(); }, danger: { label: "Cancel", className: "btn-danger", callback: function () { bootbox.hideAll(); } } } } }); }; </script>
我还使用了bootbox对话框来确认删除操作。我通过nuget添加了bootbox包,并将其添加到BundleConfig中,以便在整个项目中使用。BundleConfig.cs中bootbox的定义如下。
bundles.Add(new ScriptBundle("~/bundles/bootbox").Include("~/Scripts/bootbox.js"));
而在.Layout.cshtml中,我像这样调用它。
@Scripts.Render("~/bundles/bootbox")
现在让我们来看看Create消息视图,它看起来如下。
这是一个简单的视图,只接收消息的Subject(主题)和Message(消息)。我使用简单的验证规则来验证Subject和Message的空值。
Create.cshtml文件如下。
@model MessageBoardApp.ViewModels.MessageReplyViewModel <div class="row"> <div class="col-12"> <div class="row-fluid"> <!-- form panel 1 --> <div class="col-lg-12 col-md-12 col-sm-12"> <!-- panel start--> <div class="panel panel-default top"> <div class="panel-heading "> <h3 class="panel-title active "> Post New Message </h3> </div> </div> <!-- end panel--> <div class="panel-body"> <div class="form-horizontal container"> @using (Html.BeginForm("PostMessage", "Message", FormMethod.Post, new { @id = "form-post-message", @class = "form-horizontal" })) { <div class="form-column col-lg-12 col-md-12 col-sm-12"> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> <label class="col-sm-4 control-label">Subject</label> <div class="col-sm-8"> @Html.TextBoxFor(p => p.Message.Subject, new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.Message.Subject) </div> </div> <div class="form-group"> <label class="col-sm-4 control-label">Message</label> <div class="col-sm-8"> @Html.TextAreaFor(p => p.Message.MessageToPost, new { @rows = 8, @class = "form-control" }) @Html.ValidationMessageFor(model => model.Message.MessageToPost) </div> </div> <div class="form-group"> <label class="col-sm-4 control-label"></label> <div class="col-sm-8"> <input type="submit" class="btn btn-primary right" value="Post Message"> </div> </div> </div> } </div> </div> </div> </div> </div> </div>
创建控制器方法
我已经解释了Home Controller中用于显示消息的方法。现在让我们来看看其他方法,例如准备创建视图、发布消息和回复消息。
准备添加新消息的视图非常简单,如下所示。
public ActionResult Create() { MessageReplyViewModel vm = new MessageReplyViewModel(); return View(vm); }
当用户提交消息时,PostMessage ActionResult使用Entity Framework将消息保存到数据库。我使用了code-first方法,并将数据库迁移到了我的LocalDB服务器。这是保存已发布消息的方法。
[HttpPost] [Authorize] public ActionResult PostMessage(MessageReplyViewModel vm) { var username = User.Identity.Name; string fullName = ""; int msgid = 0; if (!string.IsNullOrEmpty(username)) { var user = dbContext.Users.SingleOrDefault(u => u.UserName == username); fullName = string.Concat(new string[] { user.FirstName, " ", user.LastName }); } Message messagetoPost = new Message(); if (vm.Message.Subject != string.Empty && vm.Message.MessageToPost != string.Empty) { messagetoPost.DatePosted = DateTime.Now; messagetoPost.Subject = vm.Message.Subject; messagetoPost.MessageToPost = vm.Message.MessageToPost; messagetoPost.From = fullName; dbContext.Messages.Add(messagetoPost); dbContext.SaveChanges(); msgid = messagetoPost.Id; } return RedirectToAction("Index", "Home", new { Id = msgid }); }
消息创建后,它会显示在主页上。当用户登录应用程序时,消息会显示在那里,他们可以查看消息并选择是否回复。
当用户点击每条消息右侧的View Reply(查看回复)按钮时,底部的面板(Replies for Message - 消息回复)将显示出来,用户可以回复该消息。
当用户键入回复并点击Reply(回复)按钮时,回复将被发送给消息所有者,告知他们他们的消息收到了回复。然后消息所有者可以登录并查看回复的详细信息。
这是回复消息的控制器方法。
[HttpPost] [Authorize] public ActionResult ReplyMessage(MessageReplyViewModel vm, int messageId) { var username = User.Identity.Name; string fullName = ""; if (!string.IsNullOrEmpty(username)) { var user = dbContext.Users.SingleOrDefault(u => u.UserName == username); fullName = string.Concat(new string[] { user.FirstName, " ", user.LastName }); } if (vm.Reply.ReplyMessage != null) { Reply _reply = new Reply(); _reply.ReplyDateTime = DateTime.Now; _reply.MessageId = messageId; _reply.ReplyFrom = fullName; _reply.ReplyMessage = vm.Reply.ReplyMessage; dbContext.Replies.Add(_reply); dbContext.SaveChanges(); } //reply to the message owner - using email template var messageOwner = dbContext.Messages.Where(x => x.Id == messageId).Select(s => s.From).FirstOrDefault(); var users = from user in dbContext.Users orderby user.FirstName select new { FullName = user.FirstName + " " + user.LastName, UserEmail = user.Email }; var uemail = users.Where(x => x.FullName == messageOwner).Select(s => s.UserEmail).FirstOrDefault(); SendGridMessage replyMessage = new SendGridMessage(); replyMessage.From = new MailAddress(username); replyMessage.Subject = "Reply for your message :" + dbContext.Messages.Where(i=>i.Id==messageId).Select(s=>s.Subject).FirstOrDefault(); replyMessage.Text = vm.Reply.ReplyMessage; replyMessage.AddTo(uemail); var credentials = new NetworkCredential("YOUR SENDGRID USERNAME", PASSWORD); var transportweb = new Web(credentials); transportweb.DeliverAsync(replyMessage); return RedirectToAction("Index", "Home", new { Id = messageId }); }
当任何用户提交回复时,都会向消息所有者发送一封通知电子邮件,以便他们能够得知有回复。
下面是一个这样的回复的例子。
删除消息和回复
如果消息列表中的任何消息与页面的上下文无关或不相关,都可以将其删除。为此,我在消息的右侧添加了一个X按钮,用户可以点击它来删除消息。我没有为该应用程序实现角色,但可以根据用户角色隐藏或显示该按钮。
我使用了bootbox进行删除确认,如下所示,消息及其回复将被删除。点击删除按钮(X)将弹出一个小窗口,用户可以在其中确认或取消对话框。如果点击Continue(继续),则该记录及其回复将从数据库中删除。
兴趣点
- ViewModel 中包含不同模型的分页列表 - 带分页和不带分页
- MVC 5, ASPNET Identity 2.0
- MessageBoardApp 可以通过进行一些修改,集成到任何具有相同 MVC 框架的页面中。
历史
2015-05-23: 初次提交