Introduction
Messaging within a web site plays an important role in communicating ideas among the users. Within the pages, there might some places where the users need to consult with others for certain matters. Rather than sending email or calling via phones, a messaging system can be added where a message can be posted and replies can be sent for the messages regarding the subject matter concerned.
Background
In this article, I will explain a simple MessageBoard App where messages can be posted/created and replies can be sent for the messages. I have used SendGrid SMTP client for sending the emails to the message owners when a reply is made.
I have used MVC 5 in Visual Studio 2013 to create the demo app where I have only used two views - one for the message display and the other one for the message creation. I have used PagedList to do the paging for displaying fixed number of rows for the messages. For using paging in views I followed the following two references for using PagedList in MVC
http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application
http://stackoverflow.com/questions/25125329/using-a-pagedlist-with-a-viewmodel-asp-net-mvc
In the first reference link above, the PageList has been used for single model, where there is a solution for using PageList in ViewModel in the later reference link where multiple models are combined and paging can be done for particular model in the ViewModel.
Description
The MessageBoard App will require user to be logged in to create messages or reply to the messages. When the user logins first time to the application the application will be resembled as below if there are already some messages created by some other users.
Below here, on the left side is the home page with some messages with replies for them. On the right side the view is for creating messages. From the home view if a user clicks Post New Message link the user will be directed to the Create page and after the user submits the page, it will redirect the user to the home with the message that has been submitted listed to the messages list.
Now let us look at how to create the app from scratch and run it.
Creating the Project
I have created a brand new MVC 5 web project in Visual Studion 2013 and named it as MessageBoard App. After the project has been created it looks as below in the Solution Explorer.
Setting the Users for the MessageBoardApp
ASPNET Identity 2.0 with MVC 5 does not uses user's Full Name in the database. In our messaging system we will use user full names to easily identify the users who are creating messages and sending replies for the messages.
For this purposes, I have created an ApplicationBaseController class and updated the .LoginPartial.cshtml so that it can display the full user name instead of the default user email.
I have a tip to the CodeProject for this at : http://www.codeproject.com/Tips/991663/Displaying-User-Full-Name-instead-of-User-Email-in
For our application the ApplicationBaseController looks as below:
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()
{
}
}
}Colourised in 25ms
Having done this, we can now create the models and viewmodels for our Messageboard App.
Creating Model
I have created two models: Message and Reply as usual in the Models fodler. The Message class looks as below:
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; }
}
}Colourised in 10ms
And the Reply class looks as below:
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; }
}
}Colourised in 9ms
There is nothing special here in the class definitions so far you can see.
Creating the ViewModel
I have created a new ViewModels folder and a new class called MessageReplyViewModel which has the following methods and properties in it as below:
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; }
}
}
}Colourised in 26ms
Notice I have used
public PagedList.IPagedList<Message> Messages { get; set; }
Colourised in 0ms
for Messages within the MessageReplyViewModel where as for Replies I have not used PagedList although both of them will be used in the same view. I will explain it in the controller action and within the view how to use the Paging.
Creating the Views
As I said earlier I will be using only two views, displaying the messages where user willl have the option to reply the messages within the same view and the other one is for creating new messages. The first view where the messages are displayed, I have used the PagedList in ViewModel where one of the model uses PagedList but the other one does not .
To use PagedList in the view we need to define the followings on the top of the view page.
@model MessageBoardApp.ViewModels.MessageReplyViewModel
@using PagedList;
@using PagedList.Mvc;
<link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />Colourised in 1ms
After that we need to setup the page size and page numbers in the controller from where the data is being passed through.
Let us have a look to the controller method.
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
}Colourised in 9ms
We can see the ActionResult above has some settings about the pagesizes and page numbers for the view. Also it counts the number of total messages that are already in the database. After setting these up we then can call them in the view as below:
Page @ViewBag.PageNumber of @ViewBag.TotalPages @Html.PagedListPager((IPagedList)ViewBag.MessagesInOnePage, page => Url.Action("Index", new { page }))Colourised in 0ms
To be clear, the output below from the home page (list of messages)
has been defined in the <div>below in Index.cshtml.
--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 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 }))
<!---->
</div>
}
</div>
--code belowColourised in 72ms
The Replies for Messages has panel has two parts: one for the Reply TextBox, Reply Button with the Message Detials which is being displayed based on the selection of View Reply from the top panel for a particular message.
The other part contains existing replies for the message with the details of each reply.
In controller method, I have defined the view for the replies as below:
--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 brevityColourised in 39ms
To render the view with the controller method, the following code serves the purpose.
--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>
<!---->
}
--code belowColourised in 88ms
I have added one more thing to the page for deleting the messages.
<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>Colourised in 8ms
The jquery method to delete the record which eventully calls the controller action is as below:
<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>Colourised in 10ms
I have also used bootbox dialog for confirming the delete action. I have added to bootbox package via nuget and added it to the BundleConfig so that it can be used for the whole project. The definition of bootbox in BundleConfig.cs as below:
bundles.Add(new ScriptBundle("~/bundles/bootbox").Include("~/Scripts/bootbox.js"));
Colourised in 1ms
And in .Layout.cshtml I have called it as:
@Scripts.Render("~/bundles/bootbox")
Colourised in 0ms
Now let us look at the Create Message view now, which will look as below:
This is a simple view only takes the message Subject and Message. I have used the simple validation rules to validate the empty values for Subject and Message.
The Create.cshtml file looks as below:
@model MessageBoardApp.ViewModels.MessageReplyViewModel
<div class="row">
<div class="col-12">
<div class="row-fluid">
<!---->
<div class="col-lg-12 col-md-12 col-sm-12">
<!---->
<div class="panel panel-default top">
<div class="panel-heading ">
<h3 class="panel-title active ">
Post New Message
</h3>
</div>
</div>
<!---->
<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>Colourised in 40ms
Creating Controller Methods
I have already explained the method in the Home Controller for displaying the message. Now let us have a look to the other methods such as preparing the create view, post message and reply message.
To prepare the view for adding new message is very simple as below:
public ActionResult Create()
{
MessageReplyViewModel vm = new MessageReplyViewModel();
return View(vm);
}
Colourised in 2ms
When a user submits a message, the PostMessage ActionResult saves the message to the database using Enitty Framework. I have used code-first approach and migrated the database to my LocalDB server. Here is the method for saving the posted message.
[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 });
}
Colourised in 20ms
When the message is created, it is displayed on the home page. When a user logins to the application the messages are displayed there and he/she can view the message and reply if he/she intends to do so.
When a user clicks View Reply button from the right side of each of the messages, the bottom panel (Replies for Message) is displayed and user can send reply for the message.
When a user types the reply and hits the Reply button, the reply is sent to the message owner to inform that a reply has been posted for his/her message. Then the message owner can login and see the details of the reply.
Here is the controller method for reply a message
[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();
}
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 });
}
Colourised in 22ms
When a reply is submitted by any user, a notification email is sent to the message owner so that he/she can be informed about the reply.
Here below is an example of such reply:
Deleting Message and Replies
Any message in the message list can be deleted if it is not related or relevant for the context of the page. To do this I have added a X button on the right side of the message so the user can click it to delete the message. I have not implemented the roles for the application, but based on user role, the button can be hidden or visible.
I have used bootbox for the delete confirmation as below where the message and the replies of the message will be deleted. Clicking the delete button (X) will popup the small window where the user can confirm or cancel the dialog. If Continue is clicked the record will be deleted from the database along with the replies for it.
Points of Interests
- PagedList for ViewModel having different models - with paging and without paging
- MVC 5, ASPNET Identity 2.0
- MessageBoardApp can be integrated with any page with little bit modifications having the same MVC frameworks
History
2015-05-23: Initial submission