Popular blog tags
Creating a New Project For the sake of consistency, we’ll go through this exercise starting with a brand-new, freshly-minted ASP.NET Core application. In Visual Studio 2017, go ahead and create a new ASP.NET Core 2 project. Here are the settings I used: Go ahead and build and run the app, just to make sure everything works. I’ll wait. Finished? Good! Now we’re ready to start adding our code! Add the Markup We’re going to use normal Bootstrap markup to display our alert message. We want this alert message to potentially be shown anywhere in our application, so it makes sense to place the message in our primary layout. I like to keep things as componetized as I can, so we’ll be making a partial view. Let’s pretend that view already exists. In /Views/Shared/_Layout.cshtml, let’s go ahead and call our (not-yet-created) partial, right before the existing RenderBody call: <div class="container body-content"> <!-- Add this line! --> @Html.Partial("_StatusMessages") <!-- ^^^^ --> @RenderBody() <hr /> <footer> <p>&copy; 2018 - HeroicSamples.BootstrapAlerts</p> </footer> </div> Copy Now create a new partial view in the /Views/Shared folder named _StatusMessages.cshtml, and replace its contents with this: <div class="alert alert-warning alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> <strong>Hello, world!</strong> This is a sample alert! </div> Copy That’s cool and all, but we don’t want our partial to show on every page with the same static text. Don’t worry though, we’ll come back and fix that shortly! “If we had an extension method, it would look like this…” The goal of this exercise is to allow us to easily attach alert messages to action results, and have those alerts show up in our UI. We’ll accomplish that using extension methods. Let’s pretend that we already have our extension methods in place. What we’d like to be able to do is something like this for our actions that return views… public IActionResult About() { ViewData["Message"] = "Your application description page."; return View().WithSuccess("It worked!", "You were able to view the about page, congrats!"); } Copy And we want the same extension method to work for methods that perform redirects, too: public IActionResult GoHome() { return RedirectToAction("Index").WithWarning("You were redirected!", "The action you hit has bounced you back to Index!"); } Copy We’re going to create extension methods that work for both of these cases. And we’ll define one extension method for each of the alert types that Bootstrap supports: return View().WithSuccess("Well done!", "You successfully read this important alert message."); return View().WithInfo("Heads up!", "This alert needs your attention, but it's not super important."); return View().WithWarning("Warning!", "Better check yourself, you're not looking too good."); return View().WithDanger("Oh snap!", "Change a few things up and try submitting again."); Copy NOTE: The names you use don’t really matter. I chose these names because I’m using Bootstrap. You could easily adapt this approach for any other CSS framework, or you could choose to decouple the extension names here from the target CSS framework entirely. It’s totally up to you! Creating the Extension Methods We know what we need to build now. Let’s make it real! We’ll start by creating a new class in our project (in a new folder!), “`/Extensions/Alerts/AlertExtensions.cs““. Here are the stubs for our extension methods: public static class AlertExtensions { public static IActionResult WithSuccess(this IActionResult result, string title, string body) { return Alert(result, "success", title, body); } public static IActionResult WithInfo(this IActionResult result, string title, string body) { return Alert(result, "info", title, body); } public static IActionResult WithWarning(this IActionResult result, string title, string body) { return Alert(result, "warning", title, body); } public static IActionResult WithDanger(this IActionResult result, string title, string body) { return Alert(result, "danger", title, body); } } Copy Each is basically the same: it calls a helper, Alert, with the parameters necessary to show the various types of alerts. That Alert method is where the magic happens! private static IActionResult Alert(IActionResult result, string type, string title, string body) { return new AlertDecoratorResult(result, type, title, body); } Copy Yep, that’s it! All done. … Oh wait, no it isn’t! AlertDecoratorResult doesn’t exist yet! But once it does, it will decorate the real action result, layering on our alert message behavior. Now, this approach isn’t perfect. One of the things I don’t like about it is that it complicates unit testing. Now your tests will have to unwrap the decorated result if they want to make any assertions about the real, underlying result. But, there are elegant ways to deal with that, which I’ll cover in a future post. For now, let’s create our actual decorator result! Creating the AlertDecoratorResult Let’s go ahead and make a new class, /Extensions/Alerts/AlertDecoratorResult.cs: public class AlertDecoratorResult : IActionResult { public IActionResult Result { get; } public string Type { get; } public string Title { get; } public string Body { get; } public AlertDecoratorResult(IActionResult result, string type, string title, string body) { Result = result; Type = type; Title = title; Body = body; } public async Task ExecuteResultAsync(ActionContext context) { //Coming in a sec! } } Copy So far, all our result is doing is grabbing the real result, the alert type, title, and body, and storing them in properties so that we can access them from our tests. We need to put those somewhere useful… somewhere that we can access them later from within our view. And for that, we’ll use TempData! In .NET Core, we can ask for a ITempDataDictionaryFactory whenever we want to access TempData. Let’s do that in our ExecuteResultAsync method, then let’s store our alert properties there! public async Task ExecuteResultAsync(ActionContext context) { //NOTE: Be sure you add a using statement for Microsoft.Extensions.DependencyInjection, otherwise // this overload of GetService won't be available! var factory = context.HttpContext.RequestServices.GetService<ITempDataDictionaryFactory>(); var tempData = factory.GetTempData(context.HttpContext); tempData["_alert.type"] = Type; tempData["_alert.title"] = Title; tempData["_alert.body"] = Body; await Result.ExecuteResultAsync(context); } Copy If you aren’t familiar with TempData, just think of it as session data that is only accessible once. It will survive one full roundtrip, even across Post-Redirect-Get requests, and that’s it. Want to know more about TempData? Check out the Microsoft docs! One question that is immediately asked when I show this technique: “why use TempData instead of ViewBag??” ViewBag works fine for ViewResults! It does not work for anything else, including redirects. Wrapping it Up: Reading the Alert in Razor We now have our alert stored in TempData, just waiting for us to use it. So let’s jump back over to our _StatusMessage.cshtml partial view, and let’s fix it so that it’s reading our alert: @{ var type = (string)TempData["_alert.type"]; var title = (string)TempData["_alert.title"]; var body = (string)TempData["_alert.body"]; } @if (!string.IsNullOrEmpty(type)) { <div class="alert alert-@type alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> <strong>@title</strong> @body </div> } Copy Next Steps I’ve put together a small sample app to show off this technique. You can clone it from its repo here: HeroicSamples.BootstrapAlerts. This code works, but there’s a lot we could do to improve it. There are some magic strings lingering in the code, and it won’t actually work with JSON results yet, either. But you get the idea! In a future post, I’ll show you how you can polish this up a bit further, how you can apply it to a JavaScript application, and how to write specs around decorated results cleanly. Stay tuned!.