Blazor

Building a blogging app with Blazor: Add Post

This post is part 4 of a series, Building a blogging app with Blazor.

Last time I added the ability to view a blog post. But the blog data was just coming from a hard-coded list. So in this post I'm going to add the ability to write a new post. And just like any good blogging platform I want to be able to write using Markdown. Lets get started.

The Server

Up till now I have been using a hard-coded list of blog posts, but I want to make things a bit more real world. The first thing I am going to do is create a new class in the server project called BlogPostService.cs with the following code.

using System;
using System.Collections.Generic;
using System.Linq;
using WordDaze.Shared;

namespace WordDaze.Server
{
    public class BlogPostService
    {
        private List<BlogPost> _blogPosts;

        public BlogPostService()
        {
            _blogPosts = new List<BlogPost>();
        }

        public List<BlogPost> GetBlogPosts() 
        {
            return _blogPosts;
        }

        public BlogPost GetBlogPost(int id) 
        {
            return _blogPosts.SingleOrDefault(x => x.Id == id);
        }

        public BlogPost AddBlogPost(BlogPost newBlogPost)
        {
            newBlogPost.Id = _blogPosts.Count + 1;
            _blogPosts.Add(newBlogPost);

            return newBlogPost;
        }
    }
}

Then I'm going to register this new service with the DI container in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddResponseCompression(options =>
    {
        options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[]
        {
            MediaTypeNames.Application.Octet,
            WasmMediaTypeNames.Application.Wasm,
        });
    });

    services.AddSingleton(typeof(BlogPostService));
}

Finally, I'm going to rework the BlogPostsController.

using WordDaze.Shared;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

namespace WordDaze.Server.Controllers
{
    public class BlogPostsController : Controller
    {
        private readonly BlogPostService _blogPostService;

        public BlogPostsController(BlogPostService blogPostService)
        {
            _blogPostService = blogPostService;
        }

        [HttpGet(Urls.BlogPosts)]
        public IActionResult GetBlogPosts()
        {
            return Ok(_blogPostService.GetBlogPosts());
        }

        [HttpGet(Urls.BlogPost)]
        public IActionResult GetBlogPostById(int id)
        {
            var blogPost = _blogPostService.GetBlogPost(id);

            if (blogPost == null)
                return NotFound();

            return Ok(blogPost);
        }
    }
}

With the changes above, I've removed the hard-coded list of blog posts. I've replaced that hard-coded list with a new service which will be responsible for managing blog posts going forward. As this is an demo app, I've scoped the BlogPostService as a singleton and I'm using a simple list as a persistence mechanism. But in a real world app the service could be backed by Entity Framework or some other ORM/data access mechanism.

I can now move on to adding the new endpoint for adding a blog post. First I'll add the route to the Urls class in the shared project.

public const string AddBlogPost = "api/blogposts";

Then the following method to the BlogPostsController.

[HttpPost(Urls.AddBlogPost)]
public IActionResult AddBlogPost([FromBody]BlogPost newBlogPost)
{
    var savedBlogPost = _blogPostService.AddBlogPost(newBlogPost);

    return Created(new Uri(Urls.BlogPost.Replace("{id}", savedBlogPost.Id.ToString()), UriKind.Relative), savedBlogPost);
}

I now have an endpoint which I can post my new blogs to. I think that will do for the server side of things. Lets move on to the client.

The Client

The first thing I'm going to do is create a folder called AddPost in the Features folder. I'm then going to create a file called AddPost.cshtml.cs.

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Services;
using WordDaze.Shared;

namespace WordDaze.Client.Features.AddPost
{
    public class AddPostModel : BlazorComponent
    {
        [Inject] private HttpClient _httpClient { get; set; }
        [Inject] private IUriHelper _uriHelper { get; set; }

        protected string Post { get; set; }
        protected string Title { get; set; }

        public async Task SavePost() 
        {
            var newPost = new BlogPost() {
                Title = Title,
                Author = "Joe Bloggs",
                Post = Post,
                Posted = DateTime.Now
            };

            var savedPost = await _httpClient.PostJsonAsync<BlogPost>(Urls.AddBlogPost, newPost);

            _uriHelper.NavigateTo($"viewpost/{savedPost.Id}");
        }
    }
}

And, as usual, a file called AddPost.cshtml.

@page "/addpost"
@layout MainLayout
@inherits AddPostModel

<WdHeader Heading="WordDaze" SubHeading="Add Post"></WdHeader>

<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div class="editor">
                <input bind=@Title placeholder="Title" class="form-control" />
                <textarea bind=@Post placeholder="Write your post" rows="25"></textarea>
                <button class="btn btn-primary float-right" onclick="@SavePost">Post</button>
            </div>
        </div>
    </div>
</div>

That is the AddPost component in place. So whats going on here?

In the model class I've added a single method which is responsible for posting the new blog post back to the server. Once this is done it will then redirect the user to view the post using the IUriHelper. This helper is provided by Blazor and allows navigation to be performed programmatically via Blazors router.

The ViewPost component itself if pretty straight forward. I've added a input for the title and bound it to the Title property on the model. And I've added a textarea which is bound to the Post property.

Spicing things up

While this is all very functional it's all a bit boring. Plain text just isn't very exciting after all. As I said at the start I want to be able to write posts using Markdown. It would also be quite nice to know exactly how many characters I've written as well. Lets start with that.

Character Counter

In order to output a live character count I'm going to use some JS Interop. The reason for this is that Blazors bind directive uses JavaScripts onchange event under the covers. And this event doesn't get fired until the element loses focus. So, if I want to see my character count change as I type then I'm going to have to resort to interop.


NOTE: Just before finishing this post I was speaking about this in the Blazor Gitter chat. And there is in-fact a way to handle this with out resorting to JS interop, but it's a little ugly. I've added it here for you. Continue reading to see the JS version.

<div class="editor">
    <input bind=@Title placeholder="Title" class="form-control" />
    <textarea bind=@Post onkeyup="this.dispatchEvent(new Event('change', { 'bubbles': true }));" placeholder="Write your post (Supports Markdown)" rows="25"></textarea>
    <div class="character-count text-blaxk-50 float-left">@(Post?.Length ?? 0) Characters</div>
    <button class="btn btn-primary float-right" onclick="@SavePost">Post</button>
</div>

This first step is to add a new JS folder in the wwwroot and then a new file called site.js.

window.wordDaze = {
    getCharacterCount: function(element) {
        return element.value.length;
    }
}

I have written a very simple function that is going to take a reference to an element and then return the length of its value property. In line with the JS interop changes which were introduced in Blazor 0.5.0. I'm attaching my function to the global window object under a scope of wordDaze.

With my JS file in place I just need to reference it in the index.html file.

<body>

    <app>Loading...</app>

    <script src="_framework/blazor.webassembly.js"></script>
    <script src="js/site.js"></script>
</body>

The last step is to make some changes in the AddPost component.

<div class="editor">
    <input bind=@Title placeholder="Title" class="form-control" />
    <textarea ref="editor" bind=@Post onkeyup="@UpdateCharacterCount" placeholder="Write your post (Supports Markdown)" rows="25"></textarea>
    <div class="character-count text-blaxk-50 float-left">@CharacterCount Characters</div>
    <button class="btn btn-primary float-right" onclick="@SavePost">Post</button>
</div>

I've added a ref attribute to the textarea. This captures a reference to this control which I can use when I call the getCharacterCount function. I'm also now binding to the onkeyup event which will call a method called UpdateCharacterCount which I'm going to add to the model class in a second. I've also added a new div to display the character count. I'm now going to make the changes to the model class.

protected int CharacterCount { get; set; }
protected ElementRef editor;

public async Task UpdateCharacterCount() => CharacterCount = await JSRuntime.Current.InvokeAsync<int>("wordDaze.getCharacterCount", editor);

I've added a new property for the character count. As well as a field to hold the reference to the textarea we saw previously. Finally I have added the UpdateCharacterCount method which calls the JS function I created earlier.

Thats it, I now have a working character counter.

Markdown Support

To wrap things up I am going to add markdown support for my posts. I'm not actually going to make any changes in the AddPost component to achieve this. All the work needs to be done when displaying the post. So I'm going to be making some changes to the ViewPost component.

The first thing I'm going to need to something to parse any markdown in my posts into valid HTML. One of the more popular .NET libraries for this is Markdig. As this is .NET Standard compatible it should work just fine so I'm going to install this into the client project.

With Markdig in place I'm going to make a slight change to the LoadBlogPost method on the ViewPost component.

private async Task LoadBlogPost() 
{
    BlogPost = await _httpClient.GetJsonAsync<BlogPost>(Urls.BlogPost.Replace("{id}", PostId));
    BlogPost.Post = Markdown.ToHtml(BlogPost.Post);
}

After getting the blog post from the API I'm running the post through Markdig in-order to convert any markdown to HTML. I now need to make one last change in the ViewPost component.

@((MarkupString)BlogPost.Post)

I'm casting the blog post to a MarkupString. This is another feature added in Blazor 0.5.0 and allows the rendering of raw HTML.

BE WARNED: Rendering raw HTML is a security risk!

That should be it, it's now possible to write a blog post using markdown. Oh! I almost forgot, I haven't added a link to be able to get to the add post page.

<ul class="navbar-nav ml-auto">
    <li class="nav-item">
        <NavLink href="/">Home</NavLink>
    </li>
    <li class="nav-item">
        <NavLink href="/addpost">Add Post</NavLink>
    </li>
</ul>

That's everything. I have omitted a couple of things from this post which are in the repo on GitHub. Things such as CSS styles and a couple of superficial tweaks to some code. I've not talked about them in the post as they're not really that interesting or relevant but feel free to check out the repo.

Wrapping up

In this post I have covered adding the ability to write new blog posts using markdown. I've also added a live character counter using JS interop as well as a non-interop version. Finally I've used the Markdig library and Blazors new MarkupString to output an HTML representation of the post. Next time I'm going to look at editing posts.

Enjoying things so far? If you have any questions or suggestions then please let me know in the comments below. As always all the source code to accompany this series is available on GitHub.