Page 3 of 4 FirstFirst 1234 LastLast
Results 21 to 30 of 40
  1. #21
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 2

    Next we want the ability for the logged in admin to edit or create posts.

    We need to edit a controller, and create a form.

    1. Edit our BuzzMMO.Web/Areas/Admin/Controllers/PostsController.cs to add in our "New", "Edit", and "Form" methods below the code from the previous post. Note that Nelson uses the same View and Method to both create and update a post. That's why there is only one view and one HttpPost method.

    Here is the entire PostsController.cs file including some commented out stuff we need later:
    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    using BuzzMMO.Web.Infrastructure;
    //using SimpleBlog.Infrastructure.Extensions;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Data;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        //[SelectedTab("posts")]
        public class PostsController : Controller
        {
            private const int PostsPerPage = 5;
    
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index(int page = 1)
            {
                var totalPostCount = _database.Posts.ToList().Count;
    
                var baseQuery = _database.Posts.OrderByDescending(f => f.CreatedAt);
    
                var postIds = baseQuery
                    .Skip((page - 1) * PostsPerPage)
                    .Take(PostsPerPage)
                    .Select(p => p.Id)
                    .ToArray();
    
                var currentPostPage = baseQuery
                    .Where(p => postIds.Contains(p.Id))
                    .Include(f => f.Tags)
                    .Include(f => f.User)
                    .ToList();
    
                return View(new PostsIndex
                {
                    Posts = new PagedData<Post>(currentPostPage, totalPostCount, page, PostsPerPage)
                });
            }
    
            public ActionResult New()
            {
                return View("Form", new PostsForm
                {
                    IsNew = true,
                    //Tags = Database.Session.Query<Tag>().Select(tag => new TagCheckBox
                    //{
                    //    Id = tag.Id,
                    //    Name = tag.Name,
                    //    IsChecked = false
                    //}).ToList()
                });
            }
    
            public ActionResult Edit(int id)
            {
                var post = _database.Posts.Find(id);
                if (post == null)
                    return HttpNotFound();
    
                return View("Form", new PostsForm
                {
                    IsNew = false,
                    PostId = id,
                    Content = post.Content,
                    Slug = post.Slug,
                    Title = post.Title,
                    //Tags = Database.Session.Query<Tag>().Select(tag => new TagCheckBox
                    //{
                    //    Id = tag.Id,
                    //    Name = tag.Name,
                    //    IsChecked = post.Tags.Contains(tag)
                    //}).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Form(PostsForm form)
            {
                // if there is no PostId then it must be a new post
                form.IsNew = form.PostId == null;
    
                if (!ModelState.IsValid)
                    return View(form);
    
                //    var selectedTags = ReconcileTags(form.Tags).ToList();
    
                Post post;
                if (form.IsNew)                         
                {
                    // New post
    
                    // You can't just use Auth.User to get the logged in user because it has cookie info added which won't save
                    var loggedInUser = Auth.User;
                    var user = _database.Users.SingleOrDefault(t => t.Id == loggedInUser.Id);
    
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = user                     
                    };
    
                    //        foreach (var tag in selectedTags)
                    //            post.Tags.Add(tag);
                }
    
                else
                {
                    // it must be an update post. Load in the post that matches the form's postid and make sure we include the user info
                    post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == form.PostId);
    
                    if (post == null)
                        return HttpNotFound();
    
                    post.UpdatedAt = DateTime.UtcNow;
    
                    //        foreach (var toAdd in selectedTags.Where(t => !post.Tags.Contains(t)))
                    //            post.Tags.Add(toAdd);
    
                    //        foreach (var toRemove in post.Tags.Where(t => !selectedTags.Contains(t)).ToList())
                    //            post.Tags.Remove(toRemove);
    
                   
                }
                post.Title = form.Title;
                post.Slug = form.Slug;
                post.Content = form.Content;
    
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
        }
    }
    2. Create our view: BuzzMMO.Web/Areas/Admin/Views/Posts/Form.cshtml
    It has some commented-out code in there we need for tags later.

    Code:
    @using System.Web.Optimization
    @model BuzzMMO.Web.Areas.Admin.ViewModels.PostsForm
    
    <h1>@(Model.IsNew ? "Create Post" : "Update Post")</h1>
    
    @using (Html.BeginForm("Form", "Posts"))
    {
        if (!Model.IsNew)
        {
            @Html.HiddenFor(f => f.PostId)
        }
    
        @Html.AntiForgeryToken()
    
        <div class="row">
            <div class="col-lg-8">
                @Html.ValidationSummary()
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Title)
                    @Html.TextBoxFor(f => f.Title, new { @class = "form-control" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Slug)
                    @Html.TextBoxFor(f => f.Slug, new { @class = "form-control", data_slug = "#Title" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Content)
                    @Html.TextAreaFor(f => f.Content, new { @class = "form-control" })
                </div>
            </div>
            <div class="col-lg-4">
                <div class="panel panel-info">
                    <div class="panel-heading">Post Actions</div>
                    <div class="panel-body">
                        <input type="submit" value="@(Model.IsNew ? "Publish Post" : "Update Post")" class="btn btn-success btn-sm" />
                        <a href="@Url.Action("Index")">or cancel</a>
                    </div>
                </div>
    
                @*<div class="panel panel-info">
                        <div class="panel-heading">Tags</div>
                        <div class="panel-body post-tag-editor">
                            <label for="new-tag-name">New Tag:</label>
                            <div class="input-group">
                                <input id="new-tag-name" type="text" class="new-tag-name form-control" />
                                <span class="input-group-btn">
                                    <button disabled class="btn btn-primary add-tag-button">Add</button>
                                </span>
                            </div>
    
                            <ul class="tag-select">
                                <li class="template">
                                    <a href="#" class="name"></a>
    
                                    <input type="hidden" class="name-input" />
                                    <input type="hidden" class="selected-input" />
                                </li>
    
                                @for (var i = 0; i < Model.Tags.Count; i++)
                                {
                                    var tag = Model.Tags[i];
    
                                    <li data-tag-id="@tag.Id" class="@(tag.IsChecked ? "selected" : "")">
                                        <a href="#">@tag.Name</a>
    
                                        <input type="hidden" name="Tags[@(i)].Id" value="@tag.Id" />
                                        <input type="hidden" name="Tags[@(i)].Name" value="@tag.Name" />
                                        <input type="hidden" name="Tags[@(i)].IsChecked" value="@tag.IsChecked.ToString()" class="selected-input" />
                                    </li>
                                }
                            </ul>
                        </div>
                    </div>*@
            </div>
        </div>
    }
    
    @*@section Scripts
        {
            @Scripts.Render("~/admin/post/scripts")
            <script src="~/Scripts/ckeditor/ckeditor.js"></script>
    
            <script>
                CKEDITOR.replace("Content");
            </script>
        }*@


    You should now be able to edit and create posts.
    ----------------------------------------------------------------------

    One further comment.

    You will notice that in the PostsController.cs file I do some messing around with finding the logged in user.
    In the original PostsController.cs file in the SimpleBlog project we create a new Post and fill it in with the User as follows:
    Code:
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = Auth.User
                    };
    This doesn't work with the Auth class in BuzzMMO because it modifies the user when we log in by adding in an authCookier to the user and saves it to cache. When we try to use Auth.User it will cause an exception in the database because it tries to create a new user as well as a new post and the user already exists.

    My workaround is to find the logged in user, but then use it to find the user in the _database.
    Code:
                   var loggedInUser = Auth.User;
                    var user = _database.Users.SingleOrDefault(t => t.Id == loggedInUser.Id);
    
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = user                     
                    };
    Last edited by oldngrey; 03-17-2017 at 10:28 PM.

  2. #22
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 3

    In the next video in the Comprehensive ASP.NET MVC series, Nelson enabled deletions as well as slugs
    This was in the video:
    Working with Data | Soft Deletion for Posts

    This was both easy and tricky to convert to the MMO project.
    The dropdown menus require some java, and the way Nelson did it was to also import Bootstrap.js. We will be doing it the same way. Bootstrap refers to it as a plugin but Bootstrap.js has all the plugins we need.

    So in summary, we have to update our PostsController, edit our admin's Form.js, find Bootstrap.js, and edit our BundleConfig.cs to bring in the bootstrap.js for our admin forms.

    1. Once again, edit BuzzMMO.Web/Areas/Admin/Controllers/PostsController.cs to add in our Trash, Delete and Restore methods. Here is the entire file. Our new methods are the last 3:
    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    using BuzzMMO.Web.Infrastructure;
    //using SimpleBlog.Infrastructure.Extensions;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Data;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        //[SelectedTab("posts")]
        public class PostsController : Controller
        {
            private const int PostsPerPage = 5;
    
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index(int page = 1)
            {
                var totalPostCount = _database.Posts.ToList().Count;
    
                var baseQuery = _database.Posts.OrderByDescending(f => f.CreatedAt);
    
                var postIds = baseQuery
                    .Skip((page - 1) * PostsPerPage)
                    .Take(PostsPerPage)
                    .Select(p => p.Id)
                    .ToArray();
    
                var currentPostPage = baseQuery
                    .Where(p => postIds.Contains(p.Id))
                    .Include(f => f.Tags)
                    .Include(f => f.User)
                    .ToList();
    
                return View(new PostsIndex
                {
                    Posts = new PagedData<Post>(currentPostPage, totalPostCount, page, PostsPerPage)
                });
            }
    
            public ActionResult New()
            {
                return View("Form", new PostsForm
                {
                    IsNew = true,
                    //Tags = Database.Session.Query<Tag>().Select(tag => new TagCheckBox
                    //{
                    //    Id = tag.Id,
                    //    Name = tag.Name,
                    //    IsChecked = false
                    //}).ToList()
                });
            }
    
            public ActionResult Edit(int id)
            {
                var post = _database.Posts.Find(id);
                if (post == null)
                    return HttpNotFound();
    
                return View("Form", new PostsForm
                {
                    IsNew = false,
                    PostId = id,
                    Content = post.Content,
                    Slug = post.Slug,
                    Title = post.Title,
                    //Tags = Database.Session.Query<Tag>().Select(tag => new TagCheckBox
                    //{
                    //    Id = tag.Id,
                    //    Name = tag.Name,
                    //    IsChecked = post.Tags.Contains(tag)
                    //}).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Form(PostsForm form)
            {
                // if there is no PostId then it must be a new post
                form.IsNew = form.PostId == null;
    
                if (!ModelState.IsValid)
                    return View(form);
    
                //    var selectedTags = ReconcileTags(form.Tags).ToList();
    
                Post post;
                if (form.IsNew)                         
                {
                    // New post
    
                    // You can't just use Auth.User to get the logged in user because it has cookie info added which won't save
                    var loggedInUser = Auth.User;
                    var user = _database.Users.SingleOrDefault(t => t.Id == loggedInUser.Id);
    
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = user                     
                    };
    
                    //        foreach (var tag in selectedTags)
                    //            post.Tags.Add(tag);
                }
    
                else
                {
                    // it must be an update post. Load in the post that matches the form's postid and make sure we include the user info
                    post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == form.PostId);
    
                    if (post == null)
                        return HttpNotFound();
    
                    post.UpdatedAt = DateTime.UtcNow;
    
                    //        foreach (var toAdd in selectedTags.Where(t => !post.Tags.Contains(t)))
                    //            post.Tags.Add(toAdd);
    
                    //        foreach (var toRemove in post.Tags.Where(t => !selectedTags.Contains(t)).ToList())
                    //            post.Tags.Remove(toRemove);
    
                   
                }
                post.Title = form.Title;
                post.Slug = form.Slug;
                post.Content = form.Content;
    
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
            
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Trash(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = DateTime.UtcNow;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Delete(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                _database.Posts.Remove(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Restore(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = null;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
        }
    }

    2. Edit BuzzMMO.Web/Areas/Admin/scripts/Forms.js to add in the slug script at the end. Here is the entire file:
    Code:
    $(document).ready(function () {
        $("a[data-post]").click(function (e) {
            e.preventDefault();
    
            var $this = $(this);
            var message = $this.data("post");
    
            if (message && !confirm(message))
                return;
    
            var antiForgeryToken = $("#anti-forgery-form input");
            var antiForgeryInput = $("<input type='hidden'>").attr("name", antiForgeryToken.attr("name")).val(antiForgeryToken.val());
    
            $("<form>")
                .attr("method", "post")
                .attr("action", $this.attr("href"))
                .append(antiForgeryInput)
                .appendTo(document.body)
                .submit();
        });
    
    
        $("[data-slug]").each(function () {
            var $this = $(this);
            var $sendSlugFrom = $($this.data("slug"));
    
            $sendSlugFrom.keyup(function () {
                var slug = $sendSlugFrom.val();
                slug = slug.replace(/[^a-zA-Z0-9\s]/g, "");
                slug = slug.toLowerCase();
                slug = slug.replace(/\s+/g, "-");
    
                if (slug.charAt(slug.length - 1) == "-")
                    slug = slug.substr(0, slug.length - 1);
    
                $this.val(slug);
            });
        });
    });
    3. Edit our BuzzMMO.Web/App_Start/BundleConfig.cs
    to include Bootstrap.js into the backendScripts.
    Here is the entire file:
    Code:
    using System.Collections.Generic;
    using System.Web.Optimization;
    using BundleTransformer.Core.Transformers;
    using BundleTransformer.Core.Translators;
    using BundleTransformer.Less.Translators;
    
    namespace BuzzMMO.Web
    {
        public static class BundleConfig
        {
            public static void RegisterBundles(BundleCollection bundles)
            {
                var frontendScripts = new ScriptBundle("~/js/frontend");
                frontendScripts.Include("~/scripts/jquery-3.1.1.js", "~/scripts/forms.js");
                frontendScripts.Transforms.Add(new JsMinify());
                bundles.Add(frontendScripts);
    
                var backendScripts = new ScriptBundle("~/js/backend");
                backendScripts.Include("~/scripts/jquery-3.1.1.js", "~/areas/admin/scripts/forms.js", "~/scripts/bootstrap.js");
                backendScripts.Transforms.Add(new JsMinify());
                bundles.Add(backendScripts);
    
                var frontendStyles = new StyleBundle("~/styles/frontend");
                frontendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                frontendStyles.Transforms.Add(new CssMinify());
                frontendStyles.Include("~/content/styles/application.less");
                bundles.Add(frontendStyles);
    
                var backendStyles = new StyleBundle("~/styles/backend");
                backendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                backendStyles.Transforms.Add(new CssMinify());
                backendStyles.Include("~/content/styles/application.less");
                bundles.Add(backendStyles);
            }
        }
    }
    4. Our last thing to do it to find and put bootstrap.js into our BuzzMMO.Web/scripts folder.

    * If you still have your bootstrap.zip file from when you did the first videos, then you can extract bootstrap.js from it.
    * If you used Nuget to install Bootstrap.less, then bootstrap.less is already in the folder and there is nothing more to do.
    * If you installed Bootstrap by following along with Nelson's videos, he cherry-picked the files from the zip so you will have to find and copy the file in yourself and include it in the project.

    I guess that bootstrap.js and bootstrap.less should be the same version.

    If you need Bootstrap, go to "http://getbootstrap.com/getting-started/#download"
    * Click either the "Download Bootstrap" or "Download source" links and open it. Find bootstrap.js.
    * Copy bootstrap.js into your BuzzMMO.Web/scripts folder.
    * In Visual Studio, click on "Show all files" in the Solution Explorer and find the new file. Right click the file and select "Include in Project".


    --------------------
    Finally, you can go to the BuzzMMO Admin page, and create, edit, trash, restore and delete posts. Notice than when you create a post, the slug will be created automatically as you type in the title.
    Last edited by oldngrey; 03-18-2017 at 03:27 AM.

  3. #23
    Join Date
    Nov 2006
    Location
    Vancouver, WA
    Posts
    236
    You are doing a great job adding more functionality to your project, although it is sad that there are so few people here.
    The only way to fail is to give up or die...

  4. #24
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 4

    In the next video in the Comprehensive ASP.NET MVC series, Nelson enabled tags
    In this post we add in the functionality from the video:
    Working with Data | Post Tag Editor

    In this video Nelson convinces Steve that tags are difficult to understand. I concur.

    We need to add or edit 10 files to get this working. As usual I will post the entire file and hopefully you can get it working with minimum difficulty - always assuming you have the same structure as my solution and are following along the web creation project faithfully.

    I will present each file in the order they appear in Sourcetree.

    1. Edit BuzzMMO.Base\Extensions\StringExtensions.cs. We add a new extension to our existing one:
    Code:
    using System.Text.RegularExpressions;
    
    namespace BuzzMMO.Base.Extensions
    {
        public static class StringExtensions
        {
            public static string TrimDoubleQuotes(this string input)
            {
                return input.StartsWith("\"") && input.EndsWith("\"") && input.Length > 1
                    ? input.Substring(1, input.Length - 2)
                    : input;
            }
    
            public static string Slugify(this string that)
            {
                that = Regex.Replace(that, @"[^a-zA-Z0-9\s]", "");
                that = that.ToLower();
                that = Regex.Replace(that, @"\s", "-");
                return that;
            }
        }
    }
    2. Edit BuzzMMO.Data\Entities\Post.cs to now include a tag constructor at the bottom:
    Code:
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace BuzzMMO.Data.Entities
    {
        public class Post
        {
            public int Id { get; set; }
    
            [Required]
            public User User { get; set; }
    
            [Required, MaxLength(128)]
            public string Title { get; set; }
    
            [Required, MaxLength(128)]
            public string Slug { get; set; }
    
            public DateTime CreatedAt { get; set; }
    
            public DateTime? UpdatedAt { get; set; }
    
            public DateTime? DeletedAt { get; set; }
    
            [Required]
            public String Content { get; set; }
    
            public virtual ICollection<Tag> Tags { get; set; }
    
    
            public virtual bool IsDeleted => DeletedAt != null;
            
            public Post()
            {
                Tags = new List<Tag>();
            }
        }
    }

    3. Edit BuzzMMO.Data\Entities\Tag.cs to now include a post constructor at the bottom:
    Code:
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace BuzzMMO.Data.Entities
    {
        public class Tag
        {
            public int Id { get; set; }
    
            [Required, MaxLength(128)]
            public string Slug { get; set; }
    
            [Required, MaxLength(128)]
            public string Name { get; set; }
    
            public virtual ICollection<Post> Posts { get; set; }
    
            public Tag()
            {
                Posts = new List<Post>();
            }
        }
    }
    4. Edit BuzzMMO.Web\App_Start\BundleConfig.cs to enable our tag editor script:
    Code:
    using System.Collections.Generic;
    using System.Web.Optimization;
    using BundleTransformer.Core.Transformers;
    using BundleTransformer.Core.Translators;
    using BundleTransformer.Less.Translators;
    
    namespace BuzzMMO.Web
    {
        public static class BundleConfig
        {
            public static void RegisterBundles(BundleCollection bundles)
            {
                var frontendScripts = new ScriptBundle("~/js/frontend");
                frontendScripts.Include("~/scripts/jquery-3.1.1.js", "~/scripts/forms.js");
                frontendScripts.Transforms.Add(new JsMinify());
                bundles.Add(frontendScripts);
    
                var backendScripts = new ScriptBundle("~/js/backend");
                backendScripts.Include("~/scripts/jquery-3.1.1.js", "~/areas/admin/scripts/forms.js", "~/scripts/bootstrap.js");
                backendScripts.Transforms.Add(new JsMinify());
                bundles.Add(backendScripts);
    
                var postEditorScripts = new ScriptBundle("~/admin/post/scripts");
                postEditorScripts.Include("~/areas/admin/scripts/posteditor.js");
                postEditorScripts.Transforms.Add(new JsMinify());
                bundles.Add(postEditorScripts);
    
                var frontendStyles = new StyleBundle("~/styles/frontend");
                frontendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                frontendStyles.Transforms.Add(new CssMinify());
                frontendStyles.Include("~/content/styles/application.less");
                bundles.Add(frontendStyles);
    
                var backendStyles = new StyleBundle("~/styles/backend");
                backendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                backendStyles.Transforms.Add(new CssMinify());
                backendStyles.Include("~/content/styles/application.less");
                bundles.Add(backendStyles);
            }
        }
    }
    5. Edit our BuzzMMO.Web\Areas\Admin\PostsController.cs to add in all the tag stuff:
    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Base.Extensions;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    using BuzzMMO.Web.Infrastructure;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Data;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        [SelectedTab("posts")]
        public class PostsController : Controller
        {
            private const int PostsPerPage = 5;
    
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index(int page = 1)
            {
                var totalPostCount = _database.Posts.ToList().Count;
    
                var baseQuery = _database.Posts.OrderByDescending(f => f.CreatedAt);
    
                var postIds = baseQuery
                    .Skip((page - 1) * PostsPerPage)
                    .Take(PostsPerPage)
                    .Select(p => p.Id)
                    .ToArray();
    
                var currentPostPage = baseQuery
                    .Where(p => postIds.Contains(p.Id))
                    .Include(f => f.Tags)
                    .Include(f => f.User)
                    .ToList();
    
                return View(new PostsIndex
                {
                    Posts = new PagedData<Post>(currentPostPage, totalPostCount, page, PostsPerPage)
                });
            }
    
            public ActionResult New()
            {
                return View("Form", new PostsForm
                {
                    IsNew = true,
                    Tags = _database.Tags.Select(tag => new TagCheckBox
                    {
                        Id = tag.Id,
                        Name = tag.Name,
                        IsChecked = false
                    }).ToList()
                });
            }
    
            public ActionResult Edit(int id)
            {
                var post = _database.Posts.Include(x => x.Tags).SingleOrDefault(x => x.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                return View("Form", new PostsForm
                {
                    IsNew = false,
                    PostId = id,
                    Content = post.Content,
                    Slug = post.Slug,
                    Title = post.Title,
                    Tags = _database.Tags.AsEnumerable().Select(t => new TagCheckBox
                    {
                        Id = t.Id,
                        Name = t.Name,
                        IsChecked = post.Tags.Contains(t)
                    }).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Form(PostsForm form)
            {
                // if there is no PostId then it must be a new post
                form.IsNew = form.PostId == null;
    
                if (!ModelState.IsValid)
                    return View(form);
    
                var selectedTags = ReconcileTags(form.Tags).ToList();
    
                Post post;
                if (form.IsNew)
                {
                    // New post
    
                    // You can't just use Auth.User to get the logged in user because it has cookie info added which won't save
                    var loggedInUser = Auth.User;
                    var user = _database.Users.SingleOrDefault(t => t.Id == loggedInUser.Id);
    
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = user
                    };
    
                    foreach (var tag in selectedTags)
                        post.Tags.Add(tag);
                }
    
                else
                {
                    // it must be an update post. Load in the post that matches the form's postid and make sure we include the user and tag info
                    post = _database.Posts.Include(t => t.User).Include(t => t.Tags).SingleOrDefault(t => t.Id == form.PostId);
    
                    if (post == null)
                        return HttpNotFound();
    
                    post.UpdatedAt = DateTime.UtcNow;
    
                    foreach (var toAdd in selectedTags.Where(t => !post.Tags.Contains(t)))
                        post.Tags.Add(toAdd);
    
                    foreach (var toRemove in post.Tags.Where(t => !selectedTags.Contains(t)).ToList())
                        post.Tags.Remove(toRemove);
                }
    
                post.Title = form.Title;
                post.Slug = form.Slug;
                post.Content = form.Content;
    
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Trash(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = DateTime.UtcNow;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Delete(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                _database.Posts.Remove(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Restore(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = null;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
    
            private IEnumerable<Tag> ReconcileTags(IEnumerable<TagCheckBox> tags)
            {
                foreach (var tag in tags.Where(t => t.IsChecked))
                {
                    if (tag.Id != null)
                    {
                        yield return _database.Tags.Find(tag.Id);
                        continue;
                    }
    
                    var existingTag = _database.Tags.FirstOrDefault(t => t.Name == tag.Name);
                    if (existingTag != null)
                    {
                        yield return existingTag;
                        continue;
                    }
    
                    var newTag = new Tag()
                    {
                        Name = tag.Name,
                        Slug = tag.Name.Slugify()
                    };
    
                    _database.Tags.Add(newTag);
                    _database.SaveChanges();
                    yield return newTag;
                }
            }
        }
    }
    6. Create a codefile BuzzMMO.Web\Areas\Admin\Scripts\PostEditor.js. I had to modify it slightly to get it to work with JQuery 3.1.1. Nelson had been using v2.
    Code:
    $(document).ready(function () {
    
        var $tagEditor = $(".post-tag-editor");
    
        $tagEditor
            .find(".tag-select")
            .on("click", "> li > a", function (e) {
                e.preventDefault();
    
                var $this = $(this);
                var $tagParent = $this.closest("li");
                $tagParent.toggleClass("selected");
    
                var selected = $tagParent.hasClass("selected");
                $tagParent.find(".selected-input").val(selected);
            });
    
        var $addTagButton = $tagEditor.find(".add-tag-button");
        var $newTagName = $tagEditor.find(".new-tag-name");
    
        $addTagButton.click(function (e) {
            e.preventDefault();
            addTag($newTagName.val());
        });
    
        $newTagName
            .keyup(function () {
                if ($newTagName.val().trim().length > 0)
                    $addTagButton.prop("disabled", false);
                else
                    $addTagButton.prop("disabled", true);
            })
            .keydown(function (e) {
                if (e.which != 13)
                    return;
    
                e.preventDefault();
                addTag($newTagName.val());
            });
     
        function addTag(name) {
            var newIndex = $tagEditor.find(".tag-select > li").length -1;
    
            $tagEditor
                .find(".tag-select > li.template")
                .clone()
                .removeClass("template")
                .addClass("selected")
                .find(".name").text(name).end()
                .find(".name-input").val(name).attr("name", "Tags[" + newIndex + "].Name").end()
                .find(".selected-input").attr("name", "Tags[" + newIndex + "].IsChecked").val(true).end()
                .appendTo($tagEditor.find(".tag-select"));
    
            $newTagName.val("");
            $addTagButton.prop("disabled", true);
        }
    });

    7. Edit BuzzMMO.Web\Areas\Admin\Views\Posts\Form.cshtml
    Code:
    @using System.Web.Optimization
    @model BuzzMMO.Web.Areas.Admin.ViewModels.PostsForm
    
    <h1>@(Model.IsNew ? "Create Post" : "Update Post")</h1>
    
    @using (Html.BeginForm("Form", "Posts"))
    {
        if (!Model.IsNew)
        {
            @Html.HiddenFor(f => f.PostId)
        }
    
        @Html.AntiForgeryToken()
    
        <div class="row">
            <div class="col-lg-8">
                @Html.ValidationSummary()
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Title)
                    @Html.TextBoxFor(f => f.Title, new { @class = "form-control" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Slug)
                    @Html.TextBoxFor(f => f.Slug, new { @class = "form-control", data_slug = "#Title" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Content)
                    @Html.TextAreaFor(f => f.Content, new { @class = "form-control" })
                </div>
            </div>
            <div class="col-lg-4">
                <div class="panel panel-info">
                    <div class="panel-heading">Post Actions</div>
                    <div class="panel-body">
                        <input type="submit" value="@(Model.IsNew ? "Publish Post" : "Update Post")" class="btn btn-success btn-sm" />
                        <a href="@Url.Action("Index")">or cancel</a>
                    </div>
                </div>
    
                <div class="panel panel-info">
                        <div class="panel-heading">Tags</div>
                        <div class="panel-body post-tag-editor">
                            <label for="new-tag-name">New Tag:</label>
                            <div class="input-group">
                                <input id="new-tag-name" type="text" class="new-tag-name form-control" />
                                <span class="input-group-btn">
                                    <button disabled class="btn btn-primary add-tag-button">Add</button>
                                </span>
                            </div>
    
                            <ul class="tag-select">
                                <li class="template">
                                    <a href="#" class="name"></a>
    
                                    <input type="hidden" class="name-input" />
                                    <input type="hidden" class="selected-input" />
                                </li>
    
                                @for (var i = 0; i < Model.Tags.Count; i++)
                                {
                                    var tag = Model.Tags[i];
    
                                    <li data-tag-id="@tag.Id" class="@(tag.IsChecked ? "selected" : "")">
                                        <a href="#">@tag.Name</a>
    
                                        <input type="hidden" name="Tags[@(i)].Id" value="@tag.Id" />
                                        <input type="hidden" name="Tags[@(i)].Name" value="@tag.Name" />
                                        <input type="hidden" name="Tags[@(i)].IsChecked" value="@tag.IsChecked.ToString()" class="selected-input" />
                                    </li>
                                }
                            </ul>
                        </div>
                    </div>
            </div>
        </div>
    }
    
    @section Scripts
        {
        @Scripts.Render("~/admin/post/scripts")
             @*<script src="~/Scripts/ckeditor/ckeditor.js"></script>
    
        <script>
                CKEDITOR.replace("Content");
            </script>*@
    }
    8. Edit BuzzMMO.Web\Areas\Admin\Views\Shared\_Layout.cshtm l
    Code:
    @using System.Web.Optimization
    
    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    
    <html>
        <head>
            <title>Buzz MMO - Admin</title>
    
            @Styles.Render("~/styles/backend")
        </head>
        <body>
            <div class="navbar navbar-default">
                <div class="container">
                    <ul class="nav navbar-nav">
                        <li>@Html.ActionLink("Settings", "Index", "Home")</li>
                        <li>@Html.ActionLink("Users", "Index", "Users")</li>
                        <li>@Html.ActionLink("Roles", "Index", "Roles")</li>
                        <li>@Html.ActionLink("Deploy Tokens", "Index", "DeployTokens")</li>
                        <li>@Html.ActionLink("Posts", "Index", "Posts")</li>
                    </ul>
                    
                    <ul class="nav navbar-nav navbar-right">
                        <li>@Html.ActionLink("Back to Home", @"Index", "Home", new { area = "" }, new { })</li>
                    </ul>
                </div>
            </div>
    
            <div class="container">
                @RenderBody()
            </div>
    
            @Scripts.Render("~/js/backend")
            @RenderSection("Scripts", false)
            
            <form class="hidden" id="anti-forgery-form">
                @Html.AntiForgeryToken()
            </form>
        </body>
    </html>
    9. Edit BuzzMMO.Web\Content\Styles\Application.less to get all the tag editor styling to look good:
    Code:
    @import '../bootstrap/bootstrap.less';
    
    body {
        background: #eee;
    }
    
    .navbar.navbar-default {
        margin-bottom: 0;
    }
    
    .field-validation-error {
        color: red;
    }
    
    .field-validation-valid {
        display: none;
    }
    
    .input-validation-error {
        border: 1px solid red;
        background-color: #ffeeee;
    }
    
    .validation-summary-errors {
        font-weight: bold;
        color: red;
    }
    
    .post-tag-editor .tag-select {
        list-style: none;
        padding: 0;
        margin: 10px 0;
    }
    
    .post-tag-editor .tag-select li {
        margin: 0;
        padding: 0;
        border-bottom: 1px dashed #eee;
    }
    
    .post-tag-editor .tag-select li.template {
        display: none;
    }
    
    .post-tag-editor .tag-select li a {
        display: block;
        padding: 2px 5px;
        border-left: 20px solid #eee;
    }
    
    .post-tag-editor .tag-select li a:hover {
        text-decoration: none;
        background: #eee;
    }
    
    .post-tag-editor .tag-select li.selected a {
        background: #d9edf7;
        border-left-color: #3a87ad;
    }
    10. Create BuzzMMO.Web\Infrastructure\SelectedTabAttribute.cs
    Code:
    using System;
    using System.Web.Mvc;
    
    namespace BuzzMMO.Web.Infrastructure
    {
        [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
        public class SelectedTabAttribute : ActionFilterAttribute
        {
            private readonly string _selectedTab;
    
            public SelectedTabAttribute(string selectedTab)
            {
                _selectedTab = selectedTab;
            }
    
            public override void OnResultExecuting(ResultExecutingContext filterContext)
            {
                filterContext.Controller.ViewBag.SelectedTab = _selectedTab;
            }
        }
    }
    After you do all that you should be able to edit and create posts with tags and create new tags.
    Last edited by oldngrey; 03-23-2017 at 10:08 PM.

  5. #25
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 5

    In the next video in the Comprehensive ASP.NET MVC series, Nelson enables a GUI post editor called ckedit.
    In this post we add in the functionality from the video:
    Working with Data | WYSIWYG Editors and Preventing XSS Attacks

    Note that the previous post about N+1 and Eager Loading was already incorporated into our scripts.

    This is a really quick mod.
    All we have to do is grab ckeditor from the web, and install it into our project, load it in view, and stop the XSS warning in our controller.

    1. Getting ckeditor.
    * Do a google search for CKEditor and click on the website of the same name.
    * Scroll right to the bottom and click on the "Download" button.
    * Download the "Standard Package".
    * Open up the zip file and see the ckeditor folder.
    * Also open up your BuzzMMO.Web\scripts folder in explorer and drag the ckeditor folder from the zip into the scripts folder in the project.
    * Then open up the BuzzMMO.Web\scripts\ckeditor folder and delete the "samples" folder from your project.
    * Back in Visual Studio do the normal thing to include a folder into the project with "Show All Files", then find the greyed out folder called ckedit, right-click it and select "Include in Project". Finally turn off 'Show All Files'.

    2. You will notice that you now have 6 errors in 3 files. This is caused by the colors trying to include an alpha channel. Simply find each color highlighted as an error and remove the last 2 FF's from the color. Easy fix especially if you have ReSharper.

    3. Edit your BuzzMMO.Web\Areas\Admin\View\Posts\Form.cshtml so that it reads: (yes just the last few lines)

    Code:
    @using System.Web.Optimization
    @model BuzzMMO.Web.Areas.Admin.ViewModels.PostsForm
    
    <h1>@(Model.IsNew ? "Create Post" : "Update Post")</h1>
    
    @using (Html.BeginForm("Form", "Posts"))
    {
        if (!Model.IsNew)
        {
            @Html.HiddenFor(f => f.PostId)
        }
    
        @Html.AntiForgeryToken()
    
        <div class="row">
            <div class="col-lg-8">
                @Html.ValidationSummary()
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Title)
                    @Html.TextBoxFor(f => f.Title, new { @class = "form-control" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Slug)
                    @Html.TextBoxFor(f => f.Slug, new { @class = "form-control", data_slug = "#Title" })
                </div>
    
                <div class="form-group">
                    @Html.LabelFor(f => f.Content)
                    @Html.TextAreaFor(f => f.Content, new { @class = "form-control" })
                </div>
            </div>
            <div class="col-lg-4">
                <div class="panel panel-info">
                    <div class="panel-heading">Post Actions</div>
                    <div class="panel-body">
                        <input type="submit" value="@(Model.IsNew ? "Publish Post" : "Update Post")" class="btn btn-success btn-sm" />
                        <a href="@Url.Action("Index")">or cancel</a>
                    </div>
                </div>
    
                <div class="panel panel-info">
                        <div class="panel-heading">Tags</div>
                        <div class="panel-body post-tag-editor">
                            <label for="new-tag-name">New Tag:</label>
                            <div class="input-group">
                                <input id="new-tag-name" type="text" class="new-tag-name form-control" />
                                <span class="input-group-btn">
                                    <button disabled class="btn btn-primary add-tag-button">Add</button>
                                </span>
                            </div>
    
                            <ul class="tag-select">
                                <li class="template">
                                    <a href="#" class="name"></a>
    
                                    <input type="hidden" class="name-input" />
                                    <input type="hidden" class="selected-input" />
                                </li>
    
                                @for (var i = 0; i < Model.Tags.Count; i++)
                                {
                                    var tag = Model.Tags[i];
    
                                    <li data-tag-id="@tag.Id" class="@(tag.IsChecked ? "selected" : "")">
                                        <a href="#">@tag.Name</a>
    
                                        <input type="hidden" name="Tags[@(i)].Id" value="@tag.Id" />
                                        <input type="hidden" name="Tags[@(i)].Name" value="@tag.Name" />
                                        <input type="hidden" name="Tags[@(i)].IsChecked" value="@tag.IsChecked.ToString()" class="selected-input" />
                                    </li>
                                }
                            </ul>
                        </div>
                    </div>
            </div>
        </div>
    }
    
    @section Scripts
        {
        @Scripts.Render("~/admin/post/scripts")
             <script src="~/Scripts/ckeditor/ckeditor.js"></script>
    
        <script>
                CKEDITOR.replace("Content");
            </script>
    }
    4. Edit your BuzzMMO.Web\Areas\Admin\Controllers\PostsControlle r.cs so that it reads: (only 1 line actually changes - it's the line just before the method "public ActionResult Form(PostsForm form)")

    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Base.Extensions;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    using BuzzMMO.Web.Infrastructure;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Data;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        [SelectedTab("posts")]
        public class PostsController : Controller
        {
            private const int PostsPerPage = 5;
    
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index(int page = 1)
            {
                var totalPostCount = _database.Posts.ToList().Count;
    
                var baseQuery = _database.Posts.OrderByDescending(f => f.CreatedAt);
    
                var postIds = baseQuery
                    .Skip((page - 1) * PostsPerPage)
                    .Take(PostsPerPage)
                    .Select(p => p.Id)
                    .ToArray();
    
                var currentPostPage = baseQuery
                    .Where(p => postIds.Contains(p.Id))
                    .Include(f => f.Tags)
                    .Include(f => f.User)
                    .ToList();
    
                return View(new PostsIndex
                {
                    Posts = new PagedData<Post>(currentPostPage, totalPostCount, page, PostsPerPage)
                });
            }
    
            public ActionResult New()
            {
                return View("Form", new PostsForm
                {
                    IsNew = true,
                    Tags = _database.Tags.Select(tag => new TagCheckBox
                    {
                        Id = tag.Id,
                        Name = tag.Name,
                        IsChecked = false
                    }).ToList()
                });
            }
    
            public ActionResult Edit(int id)
            {
                var post = _database.Posts.Include(x => x.Tags).SingleOrDefault(x => x.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                return View("Form", new PostsForm
                {
                    IsNew = false,
                    PostId = id,
                    Content = post.Content,
                    Slug = post.Slug,
                    Title = post.Title,
                    Tags = _database.Tags.AsEnumerable().Select(t => new TagCheckBox
                    {
                        Id = t.Id,
                        Name = t.Name,
                        IsChecked = post.Tags.Contains(t)
                    }).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken, ValidateInput(false)]
            public ActionResult Form(PostsForm form)
            {
                // if there is no PostId then it must be a new post
                form.IsNew = form.PostId == null;
    
                if (!ModelState.IsValid)
                    return View(form);
    
                var selectedTags = ReconcileTags(form.Tags).ToList();
    
                Post post;
                if (form.IsNew)
                {
                    // New post
    
                    // You can't just use Auth.User to get the logged in user because it has cookie info added which won't save
                    var loggedInUser = Auth.User;
                    var user = _database.Users.SingleOrDefault(t => t.Id == loggedInUser.Id);
    
                    post = new Post
                    {
                        CreatedAt = DateTime.UtcNow,
                        User = user
                    };
    
                    foreach (var tag in selectedTags)
                        post.Tags.Add(tag);
                }
    
                else
                {
                    // it must be an update post. Load in the post that matches the form's postid and make sure we include the user and tag info
                    post = _database.Posts.Include(t => t.User).Include(t => t.Tags).SingleOrDefault(t => t.Id == form.PostId);
    
                    if (post == null)
                        return HttpNotFound();
    
                    post.UpdatedAt = DateTime.UtcNow;
    
                    foreach (var toAdd in selectedTags.Where(t => !post.Tags.Contains(t)))
                        post.Tags.Add(toAdd);
    
                    foreach (var toRemove in post.Tags.Where(t => !selectedTags.Contains(t)).ToList())
                        post.Tags.Remove(toRemove);
                }
    
                post.Title = form.Title;
                post.Slug = form.Slug;
                post.Content = form.Content;
    
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Trash(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = DateTime.UtcNow;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Delete(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                _database.Posts.Remove(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Restore(int id)
            {
                var post = _database.Posts.Include(t => t.User).SingleOrDefault(t => t.Id == id);
                if (post == null)
                    return HttpNotFound();
    
                post.DeletedAt = null;
                _database.Posts.AddOrUpdate(post);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
    
            private IEnumerable<Tag> ReconcileTags(IEnumerable<TagCheckBox> tags)
            {
                foreach (var tag in tags.Where(t => t.IsChecked))
                {
                    if (tag.Id != null)
                    {
                        yield return _database.Tags.Find(tag.Id);
                        continue;
                    }
    
                    var existingTag = _database.Tags.FirstOrDefault(t => t.Name == tag.Name);
                    if (existingTag != null)
                    {
                        yield return existingTag;
                        continue;
                    }
    
                    var newTag = new Tag()
                    {
                        Name = tag.Name,
                        Slug = tag.Name.Slugify()
                    };
    
                    _database.Tags.Add(newTag);
                    _database.SaveChanges();
                    yield return newTag;
                }
            }
        }
    }
    You can now edit the posts in a wysiwyg editor. A simple one to implement at last!
    Last edited by oldngrey; 06-18-2017 at 04:16 AM.

  6. #26
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 6

    In the next video in the Comprehensive ASP.NET MVC series, Nelson enables the posts frontend screen main web page.
    In this post we add in the functionality from the video:
    Frontend - Building our Frontend Part 1 and Part 2
    This is the final set of scripts in that series.

    A few notes up front:
    I renamed application.less to backend.less because I now want the backend styles to be different to the frontend.
    It's official, converting from NHibernate to Entity Framework is harder than I expected.
    Also whereas Nelson implemented the blogs as the main page to the website, I decided to make it a menu item on our mainpage. You of course can do it any way you like.

    Overall there are 19 files to edit or create. As has become usual I will post the entire file rather than just trying to explain where the change it located.

    Each file below is in the order I see it in Sourcetree. So expect errors until the final file is done.

    1. Edit BuzzMMO.Web\App_Start\BundleConfig.src
    Code:
    using System.Collections.Generic;
    using System.Web.Optimization;
    using BundleTransformer.Core.Transformers;
    using BundleTransformer.Core.Translators;
    using BundleTransformer.Less.Translators;
    
    namespace BuzzMMO.Web
    {
        public static class BundleConfig
        {
            public static void RegisterBundles(BundleCollection bundles)
            {
                var frontendScripts = new ScriptBundle("~/js/frontend");
                frontendScripts.Include("~/scripts/jquery-3.1.1.js", "~/scripts/forms.js");
                frontendScripts.Transforms.Add(new JsMinify());
                bundles.Add(frontendScripts);
    
                var backendScripts = new ScriptBundle("~/js/backend");
                backendScripts.Include("~/scripts/jquery-3.1.1.js", "~/areas/admin/scripts/forms.js", "~/scripts/bootstrap.js");
                backendScripts.Transforms.Add(new JsMinify());
                bundles.Add(backendScripts);
    
                var postScripts = new ScriptBundle("~/posts/frontend");
                postScripts.Include("~/scripts/jquery-3.1.1.js", "~/scripts/jquery.timeago.js", "~/scripts/Frontend.js");
                postScripts.Transforms.Add(new JsMinify());
                bundles.Add(postScripts);
    
                var postEditorScripts = new ScriptBundle("~/admin/post/scripts");
                postEditorScripts.Include("~/areas/admin/scripts/posteditor.js");
                postEditorScripts.Transforms.Add(new JsMinify());
                bundles.Add(postEditorScripts);
    
                var frontendStyles = new StyleBundle("~/styles/frontend");
                frontendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                frontendStyles.Transforms.Add(new CssMinify());
                frontendStyles.Include("~/content/styles/frontend.less");
                bundles.Add(frontendStyles);
    
                var backendStyles = new StyleBundle("~/styles/backend");
                backendStyles.Transforms.Add(new StyleTransformer(new List<ITranslator>{new LessTranslator()}));
                backendStyles.Transforms.Add(new CssMinify());
                backendStyles.Include("~/content/styles/backend.less");
                bundles.Add(backendStyles);
            }
        }
    }
    2. Edit BuzzMMO.Web\App_Start\RouteConfig.cs
    Code:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;
    
    namespace BuzzMMO.Web
    {
        public class RouteConfig
        {
            public static void RegisterRoutes(RouteCollection routes)
            {
                routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
                var namespaces = new[] { "BuzzMMO.Web.Controllers" };
    
                // next 4 routes added in for simpleblog posts and tags
                routes.MapRoute("TagForRealThisTime", "tag/{idAndSlug}", new { controller = "Posts", action = "Tag" }, namespaces);
                routes.MapRoute("Tag", "tag/{id}-{slug}", new { controller = "Posts", action = "Tag" }, namespaces);
    
                routes.MapRoute("PostForRealThisTime", "post/{idAndSlug}", new { controller = "Posts", action = "Show" }, namespaces);
                routes.MapRoute("Post", "post/{id}-{slug}", new { controller = "Posts", action = "Show" }, namespaces);
                
                //buzzmmo routes
                routes.MapRoute("Download", "download/{id}", new { controller = "Download", action = "Download" }, namespaces);
                routes.MapRoute("Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces);
    
                //added in for simpleblog sidebar
                routes.MapRoute("Sidebar", "", new { controller = "Layout", action = "Sidebar" }, namespaces);
            }
        }
    }
    3. Edit BuzzMMO.Web\Areas\Admin\Controllers\UsersControlle r.cs
    Code:
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        public class UsersController : Controller
        {
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index()
            {
                return View(new UsersIndex
                {
                    // grab all the users including their roles
                    Users = _database.Users.Include(t => t.Roles).ToList()
                });
            }
    
            public ActionResult Create()
            {
                return View(new UsersCreate
                {
                    // populate all the roles in the UserRole class as a .net object so the View can read the roles with IsSelected false, and do it immediately (.ToList)
                    Roles = _database.Roles.AsEnumerable().Select(t => new UserRole(t.Id, t.Name, false)).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Create(UsersCreate form)
            {
                // check if any user has the same e-mail
                if (_database.Users.Any(t => t.Username == form.Username))
                    ModelState.AddModelError("Username", "Usernames must be unique.");
    
                if (_database.Users.Any(t => t.Email == form.Email))
                    ModelState.AddModelError("Email", "Emails must be unique.");
    
                if (!ModelState.IsValid)
                    return View(form);
    
                var user = new User
                {
                    Email = form.Email,
                    Username = form.Username,
    
                    // Create an empty Role list
                    Roles = new List<Role>()
                };
    
                user.SetPassword(form.Password);
                SyncRoles(user.Roles, form.Roles);
    
                _database.Users.Add(user);
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
    
            public ActionResult Edit(int id)
            {
                // this line is interesing as it queries for the user and includes the roles they have in one query - don't use find here. Include here does same job as Fetch in NHibernate
                //var user = _database.Users.Find(id); <--- this causes database stream already open error
                var user = _database.Users.Include(t => t.Roles).SingleOrDefault(t => t.Id == id);
                if (user == null)
                    return RedirectToAction("Index");
    
                return View(new UsersEdit
                {
                    Email = user.Email,
                    Username = user.Username,
                    Roles = _database.Roles.AsEnumerable().Select(t => new UserRole(t.Id, t.Name, user.Roles.Contains(t))).ToList()
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Edit(int id, UsersEdit form)
            {
                var user = _database.Users.Find(id);
    
                if (user == null)
                    return RedirectToAction("Index");
    
                // check if any user has the same e-mail that isn't the user we are editing
                if (_database.Users.Any(t => t.Email == form.Email && t.Id != id))
                    ModelState.AddModelError("Email", "Emails must be unique.");
    
                if (_database.Users.Any(t => t.Username == form.Username && t.Id != id))
                    ModelState.AddModelError("Username", "Usernames must be unique.");
    
                if (user.Username == "admin" && form.Username != user.Username)
                    ModelState.AddModelError("Username", "Cannot change admin username");
    
                if (Auth.User.Id == id && !form.Roles.Any(t => t.Name == "admin" && t.IsSelected))
                    ModelState.AddModelError("Username", "Cannot remove admin role from yourself");
    
                if (!ModelState.IsValid)
                    return View(form);
    
                user.Email = form.Email;
                user.Username = form.Username;
                SyncRoles(user.Roles, form.Roles);
    
                _database.SaveChanges();
                return RedirectToAction("Index");
            }
            
            public ActionResult ResetPassword(int id)
            {
                var user = _database.Users.Find(id);
                if (user == null)
                    return RedirectToAction("Index");
    
                return View(new UsersResetPassword
                {
                    Username = user.Username
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult ResetPassword(int id, UsersResetPassword form)
            {
                var user = _database.Users.Find(id);
                if (user == null)
                    return RedirectToAction("Index");
    
                form.Username = user.Username;
    
                if (!ModelState.IsValid)
                    return View(form);
    
                user.SetPassword(form.Password);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Delete(int id)
            {
                var user = _database.Users.Find(id);
                if (user == null)
                    return RedirectToAction("Index");
    
                if (user.Id == Auth.User.Id)
                    return RedirectToAction("Index");
    
                _database.Users.Remove(user);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            // get the empty Role list and populate it with the roles from the form that were ticked
            private void SyncRoles(ICollection<Role> entityRoles, IEnumerable<UserRole> formRoles)
            {
                entityRoles.Clear();
                foreach (var role in formRoles.Where(t => t.IsSelected))
                    entityRoles.Add(_database.Roles.Find(role.Id));
            }
        }
    }
    4. I renamed Application.less to Backend.less. Then edit BuzzMMO.Web\Content\Styles\Backend.less
    Code:
    @import '../bootstrap/bootstrap.less';
    
    body {
        background: #eee;
    }
    
    .navbar.navbar-default {
        margin-bottom: 0;
    }
    
    .field-validation-error {
        color: red;
    }
    
    .field-validation-valid {
        display: none;
    }
    
    .input-validation-error {
        border: 1px solid red;
        background-color: #ffeeee;
    }
    
    .validation-summary-errors {
        font-weight: bold;
        color: red;
    }
    
    .post-tag-editor .tag-select {
        list-style: none;
        padding: 0;
        margin: 10px 0;
    }
    
    .post-tag-editor .tag-select li {
        margin: 0;
        padding: 0;
        border-bottom: 1px dashed #eee;
    }
    
    .post-tag-editor .tag-select li.template {
        display: none;
    }
    
    .post-tag-editor .tag-select li a {
        display: block;
        padding: 2px 5px;
        border-left: 20px solid #eee;
    }
    
    .post-tag-editor .tag-select li a:hover {
        text-decoration: none;
        background: #eee;
    }
    
    .post-tag-editor .tag-select li.selected a {
        background: #d9edf7;
        border-left-color: #3a87ad;
    }
    5. Create BuzzMMO.Web\Content\Styles\Frontend.less
    Code:
    @import '../bootstrap/bootstrap.less';
    
    body {
        background: #eee;
    }
    
    .navbar.navbar-default {
        margin-bottom: 0;
    }
    
    .field-validation-error {
        color: red;
    }
    
    .field-validation-valid {
        display: none;
    }
    
    .input-validation-error {
        border: 1px solid red;
        background-color: #ffeeee;
    }
    
    .validation-summary-errors {
        font-weight: bold;
        color: red;
    }
    
    .site-header {
        background: #2c3e50;
        color: #eee;
        margin-bottom: 20px;
    }
    
    .site-header .page-header {
        border: none;
        padding: 0;
        margin-top: 20px;
        margin-bottom: 10px;
    }
    
    .site-header .page-header small {
        color: #ddd;
        font-size: 16pt;
    }
    
    .site-header a {
        color: #fff;
        text-decoration: none;
    }
    
    .post > h1 {
        font-size: 16pt;
        padding: 0;
        margin: 0;
        margin-bottom: 5px;
        height: 41px;
        line-height: 41px;
    }
    
    .sidebar .tags .list-group {
        margin: 0;
    }
    
    .sidebar .tags .list-group a {
        position: relative;
        background: transparent;
        border-radius: 0;
    }
    
    .sidebar .tags .list-group a .name {
        z-index: 2;
        position: relative;
    }
    
    .sidebar .tags .list-group a .badge {
        z-index: 5;
        position: relative;
    }
    
    .sidebar .tags .list-group a:hover {
        background: #ddd;
    }
    
    .sidebar .tags .progress-bar {
        background: #ddd;
        border: none;
        box-shadow: none;
    }
    
    .sidebar .tags .progress {
        display: block;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        z-index: 1;
        padding: 6px;
        margin: 0;
        background: transparent;
    }
    
    // this one causes a margin bottom error on the top menu
    /*.post {
        margin-bottom: 20px;
    }*/
    
    .post footer {
        background: #eee;
        padding: 10px;
        overflow: hidden;
        font-size: 10pt;
        color: #aaa;
    }
    
    .post .tags {
        margin: 0;
        margin-top: -1px;
    }
    
    .post .tags li {
        float: left;
        margin-right: 5px;
    }
    6. Create BuzzMMO.Web\Controllers\LayoutController.cs
    Code:
    using System.Collections.Generic;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Data;
    using BuzzMMO.Web.ViewModels;
    
    namespace BuzzMMO.Web.Controllers
    {
        public class LayoutController : Controller
        {
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            [ChildActionOnly]
            public ActionResult Sidebar()
            {
                return View(new LayoutSidebar
                {
                    IsLoggedIn = Auth.User != null,
                    Username = Auth.User != null ? Auth.User.Username : "",
                    IsAdmin = User.IsInRole("admin"),
    
                    Tags = _database.Tags.ToList().Select(tag => new
                    {
                        tag.Id,
                        tag.Name,
                        tag.Slug,
                        PostCount = tag.Posts.Count
                    })
                    .Where(t => t.PostCount > 0)
                    .OrderByDescending(p => p.PostCount)
                    .Select(tag => new SidebarTag(tag.Id, tag.Name, tag.Slug, tag.PostCount))
                    .ToList()
                });
            }
        }
    }
    7. Create BuzzMMO.Web\Controllers\PostsController.cs
    Code:
    using System;
    using System.Data.Entity;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Web.Mvc;
    using BuzzMMO.Data;
    using BuzzMMO.Web.Infrastructure;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Web.ViewModels;
    
    namespace BuzzMMO.Web.Controllers
    {
        public class PostsController : Controller
        {
            private const int PostsPerPage = 5;
    
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            // posts frontend home page
            public ActionResult Index(int page = 1)
            {
                var baseQuery = _database.Posts.Where(t => t.DeletedAt == null).OrderByDescending(f => f.CreatedAt);
    
                var totalPostCount = baseQuery.Count();
    
                var postIds = baseQuery
                    .Skip((page - 1) * PostsPerPage)
                    .Take(PostsPerPage)
                    .Select(t => t.Id)
                    .ToArray();
    
                var posts = baseQuery
                    .Where(t => postIds.Contains(t.Id))
                    .Include(t => t.Tags)
                    .Include(t => t.User)
                    .ToList();
    
                return View(new PostsIndex
                {
                    Posts = new PagedData<Post>(posts, totalPostCount, page, PostsPerPage)
                });
            }
    
            // gets run when you click on a tag to get all posts containing the tag
            public ActionResult Tag(string idAndSlug, int page = 1)
            {
                var parts = SeparateIdAndSlug(idAndSlug);
                if (parts == null)
                {
                    return HttpNotFound();
                }
    
                Tag tag = _database.Tags.Find(parts.Item1);
                if (tag == null)
                {
                    return HttpNotFound();
                }
    
                if (!tag.Slug.Equals(parts.Item2, StringComparison.CurrentCultureIgnoreCase))
                {
                    return RedirectToRoutePermanent("tag", new { id = parts.Item1, slug = tag.Slug });
                }
    
                var totalPostCount = tag.Posts.Count;
    
                var postIds = tag.Posts
                     .OrderByDescending(g => g.CreatedAt)
                     .Skip((page - 1) * PostsPerPage)
                     .Take(PostsPerPage)
                     .Where(t => t.DeletedAt == null)
                     .Select(t => t.Id)
                     .ToArray();
    
                var posts = _database.Posts
                    .OrderByDescending(b => b.CreatedAt)
                    .Where(p => postIds.Contains(p.Id))
                    .Include(f => f.Tags)
                    .Include(f => f.User)
                    .ToList();
    
                return View(new PostsTag
                {
                    Tag = tag,
                    Posts = new PagedData<Post>(posts, totalPostCount, page, PostsPerPage)
                });
            }
    
            // showing an individual post
            public ActionResult Show(string idAndSlug)
            {
                var parts = SeparateIdAndSlug(idAndSlug);
                if (parts == null)
                {
                    return HttpNotFound();
                }
    
                Post post = _database.Posts.Find(parts.Item1);
    
                if (post == null || post.IsDeleted)
                {
                    return HttpNotFound();
                }
    
                if (!post.Slug.Equals(parts.Item2, StringComparison.CurrentCultureIgnoreCase))
                {
                    return RedirectToRoutePermanent("Post", new { id = parts.Item1, slug = post.Slug });
                }
    
                return View(new PostsShow
                {
                    Post = post
                });
            }
    
            private System.Tuple<int, string> SeparateIdAndSlug(string idAndSlug)
            {
                var matches = Regex.Match(idAndSlug, @"^(\d+)\-(.*)?$");
                if (!matches.Success)
                {
                    return null;
                }
    
                var id = int.Parse(matches.Result("$1"));
                var slug = matches.Result("$2");
                return Tuple.Create(id, slug);
            }
        }
    }
    8. Create BuzzMMO.Web\Scripts\Frontend.js
    Code:
    $(document).ready(function () {
        $(".timeago").timeago();
    });
    9. Create BuzzMMO.Web\Scripts\jquery.timeago.js (or get it from the timeago website)
    Code:
    /**
     * Timeago is a jQuery plugin that makes it easy to support automatically
     * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
     *
     * @name timeago
     * @version 1.5.4
     * @requires jQuery v1.2.3+
     * @author Ryan McGeary
     * @license MIT License - http://www.opensource.org/licenses/mit-license.php
     *
     * For usage and examples, visit:
     * http://timeago.yarp.com/
     *
     * Copyright (c) 2008-2017, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
     */
    
    (function (factory) {
        if (typeof define === 'function' && define.amd) {
            // AMD. Register as an anonymous module.
            define(['jquery'], factory);
        } else if (typeof module === 'object' && typeof module.exports === 'object') {
            factory(require('jquery'));
        } else {
            // Browser globals
            factory(jQuery);
        }
    }(function ($) {
        $.timeago = function (timestamp) {
            if (timestamp instanceof Date) {
                return inWords(timestamp);
            } else if (typeof timestamp === "string") {
                return inWords($.timeago.parse(timestamp));
            } else if (typeof timestamp === "number") {
                return inWords(new Date(timestamp));
            } else {
                return inWords($.timeago.datetime(timestamp));
            }
        };
        var $t = $.timeago;
    
        $.extend($.timeago, {
            settings: {
                refreshMillis: 60000,
                allowPast: true,
                allowFuture: false,
                localeTitle: false,
                cutoff: 0,
                autoDispose: true,
                strings: {
                    prefixAgo: null,
                    prefixFromNow: null,
                    suffixAgo: "ago",
                    suffixFromNow: "from now",
                    inPast: 'any moment now',
                    seconds: "less than a minute",
                    minute: "about a minute",
                    minutes: "%d minutes",
                    hour: "about an hour",
                    hours: "about %d hours",
                    day: "a day",
                    days: "%d days",
                    month: "about a month",
                    months: "%d months",
                    year: "about a year",
                    years: "%d years",
                    wordSeparator: " ",
                    numbers: []
                }
            },
    
            inWords: function (distanceMillis) {
                if (!this.settings.allowPast && !this.settings.allowFuture) {
                    throw 'timeago allowPast and allowFuture settings can not both be set to false.';
                }
    
                var $l = this.settings.strings;
                var prefix = $l.prefixAgo;
                var suffix = $l.suffixAgo;
                if (this.settings.allowFuture) {
                    if (distanceMillis < 0) {
                        prefix = $l.prefixFromNow;
                        suffix = $l.suffixFromNow;
                    }
                }
    
                if (!this.settings.allowPast && distanceMillis >= 0) {
                    return this.settings.strings.inPast;
                }
    
                var seconds = Math.abs(distanceMillis) / 1000;
                var minutes = seconds / 60;
                var hours = minutes / 60;
                var days = hours / 24;
                var years = days / 365;
    
                function substitute(stringOrFunction, number) {
                    var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
                    var value = ($l.numbers && $l.numbers[number]) || number;
                    return string.replace(/%d/i, value);
                }
    
                var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
                    seconds < 90 && substitute($l.minute, 1) ||
                    minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
                    minutes < 90 && substitute($l.hour, 1) ||
                    hours < 24 && substitute($l.hours, Math.round(hours)) ||
                    hours < 42 && substitute($l.day, 1) ||
                    days < 30 && substitute($l.days, Math.round(days)) ||
                    days < 45 && substitute($l.month, 1) ||
                    days < 365 && substitute($l.months, Math.round(days / 30)) ||
                    years < 1.5 && substitute($l.year, 1) ||
                    substitute($l.years, Math.round(years));
    
                var separator = $l.wordSeparator || "";
                if ($l.wordSeparator === undefined) { separator = " "; }
                return $.trim([prefix, words, suffix].join(separator));
            },
    
            parse: function (iso8601) {
                var s = $.trim(iso8601);
                s = s.replace(/\.\d+/, ""); // remove milliseconds
                s = s.replace(/-/, "/").replace(/-/, "/");
                s = s.replace(/T/, " ").replace(/Z/, " UTC");
                s = s.replace(/([\+\-]\d\d)\:?(\d\d)/, " $1$2"); // -04:00 -> -0400
                s = s.replace(/([\+\-]\d\d)$/, " $100"); // +09 -> +0900
                return new Date(s);
            },
            datetime: function (elem) {
                var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
                return $t.parse(iso8601);
            },
            isTime: function (elem) {
                // jQuery's `is()` doesn't play well with HTML5 in IE
                return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
            }
        });
    
        // functions that can be called via $(el).timeago('action')
        // init is default when no action is given
        // functions are called with context of a single element
        var functions = {
            init: function () {
                functions.dispose.call(this);
                var refresh_el = $.proxy(refresh, this);
                refresh_el();
                var $s = $t.settings;
                if ($s.refreshMillis > 0) {
                    this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
                }
            },
            update: function (timestamp) {
                var date = (timestamp instanceof Date) ? timestamp : $t.parse(timestamp);
                $(this).data('timeago', { datetime: date });
                if ($t.settings.localeTitle) {
                    $(this).attr("title", date.toLocaleString());
                }
                refresh.apply(this);
            },
            updateFromDOM: function () {
                $(this).data('timeago', { datetime: $t.parse($t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title")) });
                refresh.apply(this);
            },
            dispose: function () {
                if (this._timeagoInterval) {
                    window.clearInterval(this._timeagoInterval);
                    this._timeagoInterval = null;
                }
            }
        };
    
        $.fn.timeago = function (action, options) {
            var fn = action ? functions[action] : functions.init;
            if (!fn) {
                throw new Error("Unknown function name '" + action + "' for timeago");
            }
            // each over objects here and call the requested function
            this.each(function () {
                fn.call(this, options);
            });
            return this;
        };
    
        function refresh() {
            var $s = $t.settings;
    
            //check if it's still visible
            if ($s.autoDispose && !$.contains(document.documentElement, this)) {
                //stop if it has been removed
                $(this).timeago("dispose");
                return this;
            }
    
            var data = prepareData(this);
    
            if (!isNaN(data.datetime)) {
                if ($s.cutoff === 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
                    $(this).text(inWords(data.datetime));
                } else {
                    if ($(this).attr('title').length > 0) {
                        $(this).text($(this).attr('title'));
                    }
                }
            }
            return this;
        }
    
        function prepareData(element) {
            element = $(element);
            if (!element.data("timeago")) {
                element.data("timeago", { datetime: $t.datetime(element) });
                var text = $.trim(element.text());
                if ($t.settings.localeTitle) {
                    element.attr("title", element.data('timeago').datetime.toLocaleString());
                } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
                    element.attr("title", text);
                }
            }
            return element.data("timeago");
        }
    
        function inWords(date) {
            return $t.inWords(distance(date));
        }
    
        function distance(date) {
            return (new Date().getTime() - date.getTime());
        }
    
        // fix for IE6 suckage
        document.createElement("abbr");
        document.createElement("time");
    }));
    10. Create BuzzMMO.Web\ViewModels\Layout.cs
    Code:
    using System.Collections.Generic;
    
    namespace BuzzMMO.Web.ViewModels
    {
        public class SidebarTag
        {
            public int Id { get; private set; }
            public string Name { get; private set; }
            public string Slug { get; private set; }
            public int PostCount { get; private set; }
    
            public SidebarTag(int id, string name, string slug, int postCount)
            {
                Id = id;
                Name = name;
                Slug = slug;
                PostCount = postCount;
            }
        }
    
        public class LayoutSidebar
        {
            public bool IsLoggedIn { get; set; }
            public string Username { get; set; }
            public bool IsAdmin { get; set; }
            public IEnumerable<SidebarTag> Tags { get; set; }
        }
    }
    11. Create BuzzMMO.Web\ViewModesl\Posts.cs
    Code:
    using BuzzMMO.Web.Infrastructure;
    using BuzzMMO.Data.Entities;
    
    namespace BuzzMMO.Web.ViewModels
    {
        public class PostsIndex
        {
            public PagedData<Post> Posts { get; set; }
        }
    
        public class PostsShow
        {
            public Post Post { get; set; }
        }
    
        public class PostsTag
        {
            public Tag Tag { get; set; }
            public PagedData<Post> Posts { get; set; }
        }
    }
    12. Create BuzzMMO.Web\Views\Layout\Sidebar.cshtml
    Code:
    @model BuzzMMO.Web.ViewModels.LayoutSidebar
    
    @{
        Layout = null;
    }
    
    <div class="panel panel-success">
        <div class="panel-heading">About Me</div>
        <div class="panel-body">
            I am a person and I do things
        </div>
    </div>
    
    @if (Model.Tags.Any())
    {
        var maxPostsInTag = Model.Tags.Max(t => t.PostCount);
    
        <div class="panel panel-info tags">
            <div class="panel-heading">Tags</div>
            <div class="panel-body">
                <div class="list-group">
                    @foreach (var tag in Model.Tags)
                    {
                        var percent = Math.Ceiling((float)tag.PostCount / maxPostsInTag * 100);
    
                        <a href="@Url.RouteUrl("Tag", new {tag.Id, tag.Slug})" class="list-group-item">
                            <span class="name">@tag.Name</span>
                            <span class="badge">@tag.PostCount</span>
                            <span class="progress">
                                <span class="progress-bar progress-bar-info" style="width: @percent%"></span>
                            </span>
                        </a>
                    }
                </div>
            </div>
        </div>
    }
    
    @if (Model.IsLoggedIn && Model.IsAdmin)
    {
        <div class="panel panel-default">
            <div class="panel-heading">Welcome back @Model.Username</div>
            <div class="panel-body">
                <div class="btn-group btn-group-sm">
                    <a href="@Url.Action("index", "posts", new {area = "admin"})" class="btn btn-default">Posts</a>
                    <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
                        <span class="caret"></span>
                    </button>
                    <ul class="dropdown-menu">
                        <li>
                            <a href="@Url.Action("new", "posts", new{area="admin"})">Create Post</a>
                        </li>
                    </ul>
                </div>
            </div>
        </div>
    }
    13 Create BuzzMMO.Web\View\Posts\Index.cshtml
    Code:
    @model BuzzMMO.Web.ViewModels.PostsIndex
    
    
    @{
        Layout = "~/Views/Posts/PostLayout.cshtml";
    }
    
    
    @{
        ViewBag.Title = "Posts";
    }
    
    @Html.Partial("_Posts", Model.Posts)
    14. Create BuzzMMO.Web\Views\Posts\PostLayout.cshtml
    Code:
    @using System.Web.Optimization
    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title - Blog</title>
    
        @Styles.Render("~/styles/frontend")
    </head>
    
    <body>
        <div class="site-header">
            <header class="container">
                <h1 class="page-header">
                    <a href="@Url.Action("Index", "Home")" >Awesome Stuff</a>
                    <small>a simple blog powered by ASP.net MVC</small>
                </h1>
            </header>
        </div>
    
        <div class="container">
            <div class="row">
                <div class="col-lg-8">
                    @RenderBody()
                </div>
    
                <div class="col-lg-4 sidebar">
                    
                    @Html.Action("Sidebar", "Layout")
    
                </div>
            </div>
    
            <footer>
                &copy; @DateTime.UtcNow.Year
            </footer>
        </div>
    
        @Scripts.Render("~/posts/frontend")
        @RenderSection("Scripts", false)
    </body>
    </html>
    15. Create BuzzMMO.Web\Views\Posts\Show.cshtml
    Code:
    @model BuzzMMO.Web.ViewModels.PostsShow
    
    
    @{
        Layout = "~/Views/Posts/PostLayout.cshtml";
    }
    
    @{
        ViewBag.Title = Model.Post.Title;
    }
    
    @Html.Partial("_Post", Model.Post)
    16. Create BuzzMMO.Web\Views\Posts\Tag.cshtml
    Code:
    @model BuzzMMO.Web.ViewModels.PostsTag
    
    @{
        Layout = "~/Views/Posts/PostLayout.cshtml";
    }
    
    @{
        ViewBag.Title = Model.Tag.Name;
    }
    
    <div class="panel panel-warning">
        <div class="panel-heading">
            Showing posts tagged with <strong>@Model.Tag.Name</strong>.
            <a href="@Url.Action("Index", "Posts")" class="pull-right">show all</a>
        </div>
    </div>
    
    @Html.Partial("_Posts", Model.Posts)
    17. Create the partial BuzzMMO.Web\View\Posts\_Post.cshtml
    Code:
    @model BuzzMMO.Data.Entities.Post
    
    <article class="post">
        <h1>
            <a href="@Url.RouteUrl("Post", new { Model.Id, Model.Slug })">@Model.Title</a>
        </h1>
        <div>
            @Html.Raw(Model.Content)
        </div>
        <footer>
            <ul class="pull-right list-unstyled tags">
                @foreach (var tag in Model.Tags)
                {
                    <li>
                        <a class="badge" href="@Url.RouteUrl("Tag", new {tag.Id, tag.Slug})">@tag.Name</a>
                    </li>}
            </ul>
            Posted: <abbr class="timeago" title="@Model.CreatedAt.ToString("o")"></abbr>
        </footer>
    </article>
    18. Create the partial BuzzMMO.Web\View\Posts\_Posts.cshtml
    Code:
    @model BuzzMMO.Web.Infrastructure.PagedData<BuzzMMO.Data.Entities.Post>
    
    @foreach (var post in Model)
    {
        @Html.Partial("_Post", post)
    }
    
    <ul class="list-unstyled">
        @if (Model.HasPreviousPage)
        {
            <li class="pull-left">
                <a href="@Url.Action(null, new {page = Model.PreviousPage})" class="btn btn-primary">
                    <i class="glyphicon glyphicon-chevron-left"></i>
                    Newer Posts
                </a>
            </li>
        }
    
        @if (Model.HasNextPage)
        {
            <li class="pull-right">
                <a href="@Url.Action(null, new {page = Model.NextPage})" class="btn btn-primary">
                    Older Posts
                    <i class="glyphicon glyphicon-chevron-right"></i>
                </a>
            </li>
        }
    </ul>
    19. Edit BuzzMMO.Web\Views\Shared\_Layout.cshtml
    Code:
    @using System.Web.Optimization
    
    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    
    <html>
        <head>
            <title>Buzz MMO</title>
    
            @Styles.Render("~/styles/frontend")
        </head>
        <body>
            <div class="navbar navbar-default">
                <div class="container">
                    <ul class="nav navbar-nav">
                        @if (User.IsInRole("registered"))
                        {
                            <li>@Html.ActionLink("Home", "Index", "home")</li>
                            <li>@Html.ActionLink("Blog", "Index", "Posts")</li>
                        }
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        @if (User.IsInRole("admin"))
                        {
                            <li>@Html.ActionLink("Admin", "Index", "Home", new { area = "admin" }, new { })</li>
                        }
    
                        @if (User.Identity.IsAuthenticated)
                        {
                            <li>@Html.ActionLink("Logout", "Logout", "auth", new {}, new {@class = "post"})</li>
                        }
                        else
                        {
                            <li>@Html.ActionLink("Register", "Register", "Register")</li>
                            <li>@Html.ActionLink("Login", "Login", "auth")</li>
                        }
                    </ul>
                </div>
            </div>
    
            <div class="container">
                @RenderBody()
            </div>
    
            @Scripts.Render("~/js/frontend")
        </body>
    </html>

    You should now see a link on the main page to the blogs and see the nice blogs frontend that Nelson made. It all appears to work and does look rather pretty!
    Last edited by oldngrey; 03-29-2017 at 06:34 PM.

  7. #27
    Join Date
    Feb 2014
    Posts
    277
    Getting Posts to work part 7

    This is really an addition to the project to enable a tag editor in much the same way as we have a roles editor.
    It only took 10 minutes to do because all I really did was copy and paste the equivalent "roles" files and replace "role" with "tag" as appropriate. Then all that was required was to add in the .slugify extension to the new tag name before saving it.

    Only 8 files needed to be changed or added. Here are the files in their entirety.

    1. Edit BuzzMMO.Web\Areas\Admin\Views\Shared\_Layout.cshtm l
    (dunno why this forum insists on replacing "cshtml" with "cshtm l")
    Code:
    @using System.Web.Optimization
    
    @{
        Layout = null;
    }
    
    <!DOCTYPE html>
    
    <html>
        <head>
            <title>Buzz MMO - Admin</title>
    
            @Styles.Render("~/styles/backend")
        </head>
        <body>
            <div class="navbar navbar-default">
                <div class="container">
                    <ul class="nav navbar-nav">
                        <li>@Html.ActionLink("Settings", "Index", "Home")</li>
                        <li>@Html.ActionLink("Users", "Index", "Users")</li>
                        <li>@Html.ActionLink("Roles", "Index", "Roles")</li>
                        <li>@Html.ActionLink("Deploy Tokens", "Index", "DeployTokens")</li>
                        <li>@Html.ActionLink("Posts", "Index", "Posts")</li>
                        <li>@Html.ActionLink("Tags", "Index", "Tags")</li>
                    </ul>
                    
                    <ul class="nav navbar-nav navbar-right">
                        <li>@Html.ActionLink("Back to Home", @"Index", "Home", new { area = "" }, new { })</li>
                    </ul>
                </div>
            </div>
    
            <div class="container">
                @RenderBody()
            </div>
    
            @Scripts.Render("~/js/backend")
            @RenderSection("Scripts", false)
            
            <form class="hidden" id="anti-forgery-form">
                @Html.AntiForgeryToken()
            </form>
        </body>
    </html>
    2. Create BuzzMMO.Web\Areas\Admin\Controllers\TagsController .cs
    Code:
    using System.Linq;
    using System.Web.Mvc;
    using MMO.Base.Extensions;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    using BuzzMMO.Web.Areas.Admin.ViewModels;
    
    namespace BuzzMMO.Web.Areas.Admin.Controllers
    {
        [Authorize(Roles = "admin")]
        public class TagsController : Controller
        {
            private readonly MMODatabaseContext _database = new MMODatabaseContext();
    
            public ActionResult Index()
            {
                return View(new TagsIndex
                {
                    Tags = _database.Tags.ToList()
                });
            }
    
            public ActionResult Create()
            {
                return View(new TagsCreate
                {
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Create(TagsCreate form)
            {
                if (!ModelState.IsValid)
                    return View(form);
    
                var tag = new Tag
                {
                    Name = form.Name,
                    Slug = form.Name.Slugify()
                };
    
                _database.Tags.Add(tag);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Delete(int id)
            {
                var tag = _database.Tags.Find(id);
                if (tag == null)
                    return HttpNotFound();
    
                _database.Tags.Remove(tag);
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
    
            public ActionResult Edit(int id)
            {
                var tag = _database.Tags.Find(id);
                if (tag == null)
                    return HttpNotFound();
    
                return View(new TagsEdit
                {
                    Name = tag.Name
                });
            }
    
            [HttpPost, ValidateAntiForgeryToken]
            public ActionResult Edit(int id, TagsEdit form)
            {
                var tag = _database.Tags.Find(id);
                if (tag == null)
                    return HttpNotFound();
    
                if (!ModelState.IsValid)
                    return View(form);
    
                tag.Name = form.Name;
                tag.Slug = form.Name.Slugify();
                _database.SaveChanges();
    
                return RedirectToAction("Index");
            }
        }
    }
    3. Create BuzzMMO.Web\Areas\Admin\ViewModels\TagsCreate.cs
    Code:
    using System.ComponentModel.DataAnnotations;
    
    namespace BuzzMMO.Web.Areas.Admin.ViewModels
    {
        public class TagsCreate
        {
            [Required, MaxLength(128)]
            public string Name { get; set; }
        }
    }
    4. Create BuzzMMO.Web\Areas\Admin\ViewModels\TagsEdit.cs
    Code:
    using System.ComponentModel.DataAnnotations;
    
    namespace BuzzMMO.Web.Areas.Admin.ViewModels
    {
        public class TagsEdit
        {
            [Required, MaxLength(128)]
            public string Name { get; set; }
        }
    }
    5. Create BuzzMMO.Web\Areas\Admin\ViewModels\TagsIndex.cs
    Code:
    using System.Collections.Generic;
    using BuzzMMO.Data.Entities;
    
    namespace BuzzMMO.Web.Areas.Admin.ViewModels
    {
        public class TagsIndex
        {
            public IEnumerable<Tag> Tags { get; set; }
        }
    }
    6. Create BuzzMMO.Web\Areas\Admin\Views\Tags\Create.cshtml
    Code:
    @model BuzzMMO.Web.Areas.Admin.ViewModels.TagsCreate
    
    <h1>Create Tag</h1>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
        @Html.ValidationSummary()
    
        <div class="form-group">
            @Html.LabelFor(f => f.Name)
            @Html.TextBoxFor(f => f.Name, new { @class = "form-control" })
        </div>
    
        <p>
            <input type="submit" value="Create Tag" class="btn btn-default btn-small" />
            or
            @Html.ActionLink("go back", "Index")
        </p>
    }
    7. Create BuzzMMO.Web\Areas\Admin\Views\Tags\Edit.cshtml

    Code:
    @model  BuzzMMO.Web.Areas.Admin.ViewModels.TagsEdit
    
    <h1>Edit Tag @Model.Name</h1>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
        @Html.ValidationSummary()
    
        <div class="form-group">
            @Html.LabelFor(f => f.Name)
            @Html.TextBoxFor(f => f.Name, new { @class = "form-control" })
        </div>
    
        <p>
            <input type="submit" value="Update Tag" class="btn btn-default btn-small" />
            or
            @Html.ActionLink("go back", "Index")
        </p>
    }
    8. Create BuzzMMO.Web\Areas\Admin\Views\Tags\Index.cshtml
    Code:
    @model BuzzMMO.Web.Areas.Admin.ViewModels.TagsIndex
    
    <h1>Tags</h1>
    
    <p>
        @Html.ActionLink("Create Tag", "Create", new { }, new { @class = "btn btn-default btn-sm" })
    </p>
    
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var tag in Model.Tags)
            {
                <tr>
                    <td>@tag.Id</td>
                    <td>@tag.Name</td>
                    <td>
                        <div class="btn-group">
                            <a href="@Url.Action("Edit", new {tag.Id})" class="btn btn-xs btn-primary">
                                <i class="glyphicon glyphicon-edit"></i>
                                edit
                            </a>
                            <a href="@Url.Action("Delete", new {tag.Id})" class="btn btn-xs btn-danger post" data-post="Are you sure you want to delete @tag.Name?">
                                <i class="glyphicon glyphicon-remove"></i>
                                delete
                            </a>
                        </div>
    
                    </td>
                </tr>
            }
        </tbody>
    </table>
    This should now completely finish all the post and tags stuff. Now on to the joys of MSBuild in the next MMO video....
    Last edited by oldngrey; 06-19-2017 at 09:16 AM.

  8. #28
    Join Date
    Feb 2014
    Posts
    277
    027 MSBuild Introduction

    No problems with the first of the MSBuild videos.
    I did notice the header of the BuzzMMO.Data.proj was a fair bit different from the one Nelson typed in for the new BuzzMMOBuild.proj. Nelson's ToolsVersion was 4.0 using Visual Studio 2012 whereas it's now up to 15.0 when using Visual Studio 2017.

    Therefore I used the first 4 lines from BuzzMMO.Data.proj in our new BuzzMMOBuild.proj file replacing his first line.
    One thing I've noticed is that newer versions of things often come with more headers and config entries. Best to use them as the model rather than old stuff from the 2012 version.
    Last edited by oldngrey; 03-29-2017 at 10:17 PM.

  9. #29
    Join Date
    Feb 2014
    Posts
    277
    028 Deploying our Launcher with MSDeploy

    Oh the joys of MSBuild. It really helps when it works the first time. I saved the command line used to upload the launcher. I have a feeling it's going to be useful again.

  10. #30
    Join Date
    Feb 2014
    Posts
    277
    029 Deploying our Client with MSBuild

    If the last video worked, this one will too. Only 2 files edited and as long as you avoid the curse of the "/" you will be fine.

Page 3 of 4 FirstFirst 1234 LastLast

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •