Blazor

Building a blogging app with Blazor: View Post

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

In the last post I built the home page of the app. I added a WebAPI endpoint which returned some hardcoded blog posts. Then built a couple of Blazor components to get that data and display it.

In this post I'm going to build the ability to view a blog post. Following a similar format to last time. I'll add a new endpoint which will return a blog post then move over to the Blazor side.

The Api

First off I'm going to add a new constant to the Url file in the shared project. This will be the route for the new endpoint.

public const string BlogPost = "api/blogposts/{id}";

With that in place I'm going to add the following code the to BlogPostsController.

[HttpGet(Urls.BlogPost)]
public IActionResult GetBlogPostById(int id) 
{
    var blogPost = _blogPosts.SingleOrDefault(x => x.Id == id);

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

    return Ok(blogPost);
}

All I'm doing here is attempting to find a blog post with the id specified. If I can't find one I'm returning a 404 not found, otherwise I'm returning the blog post.

The Client

With the API in place, I'm going to add a new folder called ViewPost in the Features folder. I'm then going to add a new file, ViewPost.cshtml, with the following code.  

@page "/viewpost/{postId}"
@layout MainLayout
@inherits ViewPostModel

<header class="masthead" style="background-image: url('/img/home-bg.jpg')">
    <div class="overlay"></div>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-md-10 mx-auto">
                <div class="post-heading">
                    <h1>@BlogPost.Title</h1>
                    <span class="meta">Posted by <a href="#">@BlogPost.Author</a> on @BlogPost.Posted.ToShortDateString()</span>
                </div>
            </div>
        </div>
    </div>
</header>

<article>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-md-10 mx-auto">
                @BlogPost.Post
            </div>
        </div>
    </div>
</article>

First, I'm defining the route for this component using the @page directive. I've then added a route parameter into the template to capture the ID of the post to view. I'm then rendering some pieces of meta data from the blog post in the <header> element. Finally, I'm outputting the actual blog post data.

As with the home component, the heavy lifting is being done in the ViewPostModel class, which this view is inheriting from. Let's take a look at that.

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

namespace WordDaze.Client.Features.ViewPost
{
    public class ViewPostModel : BlazorComponent 
    {
        [Inject] private HttpClient _httpClient { get; set; }

        [Parameter] protected string PostId { get; set; }

        protected BlogPost BlogPost { get; set; } = new BlogPost();

        protected override async Task OnInitAsync()
        {
            await LoadBlogPost();
        }

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

Similar to the home component in the previous post, I'm injecting a HTTP client and overriding the OnInitAsync method. I make my call to the API and load the specified blog post.

I've added a PostId property which matches the route parameter I defined in the route template above. As with all component arguments in Blazor, they must be decorated with the [Parameter] attribute and be non-public.  I then use this value in the LoadBlogPost method to call the API.

Time to refactor

I've noticed I've duplicated some markup. Both the Home and ViewPost components are using almost exactly the same code for the header. This is a great opportunity to refactor that code into a shared component.

using System;
using Microsoft.AspNetCore.Blazor.Components;

namespace WordDaze.Client.Shared
{
    public class WdHeaderModel : BlazorComponent
    {
        [Parameter] protected string Heading { get; set; }
        [Parameter] protected string SubHeading { get; set; }
        [Parameter] protected string Author { get; set; }
        [Parameter] protected DateTime PostedDate { get; set; }
    }
}

I'm starting by creating the model class for my new shared header component. As you can see it's pretty simple. I've identified the different arguments I need the component to take and declared them as properties, remembering to decorate them with the parameter attribute.

@inherits WdHeaderModel

<header class="masthead" style="background-image: url('/img/home-bg.jpg')">
    <div class="overlay"></div>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-md-10 mx-auto">
            <div class="@((Author != null && PostedDate != null) ? "post-heading" : "site-heading" )">
                <h1>@Heading</h1>
                @if (SubHeading != null)
                {
                <span class="subheading">@SubHeading</span>
                }
                @if (Author != null && PostedDate != null) 
                {
                <span class="meta">Posted by <a href="#">@Author</a> on @PostedDate.ToShortDateString()</span>
                }
            </div>
            </div>
        </div>
    </div>
</header>
``
`

I've replaced the hardcoded values with razor code which will print the values of the components arguments. I noted that there are two combinations of arguments I'm going to need to pass into this new component. The first, when on the Home component I need to pass a heading and a sub heading. The second, when I'm on the ViewPost component and I need to pass a heading, an author and a posted date. With this in mind I've added some checks so I don't try to print out anything that may not have a value.

If you're wondering why I've called the component WdHeader, it's so I don't collide with the HTML <header> element. Currently with Blazor you can't name your components the same as existing HTML elements but differ them with case, as you can in libraries like React. This is something the team have said they will look at implementing but it's currently not a priority.

Now I have the new header component I just need to refactor the Home and ViewPost components to use it.

@page "/"
@layout MainLayout
@inherits HomeModel

<WdHeader Heading="WordDaze" SubHeading="A Blazor Powered Blogging App"></WdHeader>

<div class="container">
    <div class="row">
        <div class="col-lg-8 col-md-10 mx-auto">
            @foreach (var post in blogPosts)
            {
                <BlogPostPreview BlogPost=@post></BlogPostPreview>
                <hr />
            }
        </div>
    </div>
</div>

@page "/viewpost/{postId}"
@layout MainLayout
@inherits ViewPostModel

<WdHeader Heading=@blogPost.Title Author=@blogPost.Author PostedDate=@blogPost.Posted></WdHeader>

<article>
    <div class="container">
        <div class="row">
            <div class="col-lg-8 col-md-10 mx-auto">
                @blogPost.Post
            </div>
        </div>
    </div>
</article>

Oh! I almost forgot, I also need to add the WordDaze.Client.Shared namespace to the _ViewImports.cshtml. This will save me having to add using statements to the Home and ViewPost components.

The finished article now looks like this.

Wrapping Up

I'm going to wrap things up there. I'm happy with the progress so far, the app is now able to list blog posts and to view specific posts. Everything is still very basic at the moment but don't worry I'll be adding a bit more flare in the next few posts. Next time I'm going to add the functionality for writing a blog post.

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.