Custom Tag Helper: Toggling Visibility On Existing HTML elements

A lot of times we need a way to toggle the visibility of an element based on some server-side values.  The most common example is if someone is logged in, show them more options than someone who is not logged in.

Option without Tag Helpers


@if (User.Identity.IsAuthenticated)
{
<div>
This should only be visible if you're logged in
</div>
}

view raw

Index.cshtml

hosted with ❤ by GitHub

This solution always felt a little dirty to me

What I’d like to do is to add an attribute off of div to toggle visibility.  So instead I’d like something that looks like this:


<div is-visible="User.Identity.IsAuthenticated">
This should only be visible if you're logged in
</div>

view raw

Index.cshtml

hosted with ❤ by GitHub

So let’s create a custom Tag Helper to do this

  • Create a TagHelpers folder in the root of your MVC project
  • Create a new class called VisibilityTagHelper
  • Copy and paste the super simple code below


// Add more target elements here on a new line if you want to target more than just div. Example: [HtmlTargetElement("a")] to hide/show links
[HtmlTargetElement("div")]
public class VisibilityTagHelper : TagHelper
{
// default to true otherwise all existing target elements will not be shown, because bool's default to false
public bool IsVisible { get; set; } = true;
// You only need one of these Process methods, but just showing the sync and async versions
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!IsVisible)
output.SuppressOutput();
base.Process(context, output);
}
public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (!IsVisible)
output.SuppressOutput();
return base.ProcessAsync(context, output);
}
}

  • Go to /Views/_ViewImports.cshtml and add a line for all the TagHelpers in your project by doing @addTagHelper *,  .  Example below:


@addTagHelper *, WebApplication1

  • Build your project *this is important or you won’t get intellisense*
  • Start using it such as the example shown below (same as above)


<div is-visible="User.Identity.IsAuthenticated">
This should only be visible if you're logged in
</div>

view raw

Index.cshtml

hosted with ❤ by GitHub

  • That’s it!

 

Or view the GitHub Commit that makes this happen

I also have a repo, where the first two commits are just the initial commit with a File => New Project.  This 3rd commit outlines all the changes needed to make to get this to work if you’d rather learn via a Commit.

 

Breaking Down The Code

The code for the Visibility Tag Helper is pretty straight forward, but here’s a breakdown of it:

  • HtmlTargetElement
    • You first decorate your class with the HtmlTargetElement attribute to tell it that you’re targeting div’s.
      [HtmlTargetElement("div")]
    • You can target more than just divs by adding more attributes.
      [HtmlTargetElement("div")]
      [HtmlTargetElement("a")]
    • You could add the is-visible attribute to ALL elements by using *.  This could be quite handy
      [HtmlTargetElement("*")]
    • UPDATE: 10/4/2018 – Per Andrew’s comment below, you can have this target all elements like above, but skip going through this TagHelper rendering by saying only run it if they have the is-visible attribute.  This should save some perf as well as not give you purple tags in Visual Studio on all of your HTML elements.
    • [HtmlTargetElement("*", Attributes = "is-visible")]
  • Inherit from TagHelper.
    • Naming the TagHelper doesn’t matter, as we are choosing the HtmlTargetElement.  If we were making our own tag, then the tag would be based off of the name.
      public class VisibilityTagHelper : TagHelper
      
  • Add a property to store the visibility of the tag.
    • We need to default it to true, otherwise none of the HTML tags specified in the HtmlTargetElement will be visible, because booleans default to false.  THAT IS REALLY IMPORTANT.  The name will be kebab-cased which means each capital letter will be split by a dash.  Example IsVisible becomes is-visible when using the tag.
      public bool IsVisible { get; set; } = true;
      
  •   Override the Process method
    • If the tag is set to not IsVisible, then we need to supress the output which will not render the tag at all.  (Thanks to Damian Edwards for the tip about SuppressOutput())
      public override void Process(TagHelperContext context, TagHelperOutput output)
      {
          if (!IsVisible)
              output.SuppressOutput();
          base.Process(context, output);
       }
      
  • That’s about it!  Super simple.

 

Update 1/3/2017: As mentioned above – thanks to Damian Edwards for informing me of the output.SuppressOutput() method.  That made things simpler.

How to fix a failure in upgrading from ASP.NET Core 1.0.x to ASP.NET Core 1.1

Error I received

  1. The project has not been restored or restore failed – run dotnet restore

  2. The project does not list one of ‘win10-x64, win81-x64, win8-x64, win7-x64’ in the ‘runtimes’ section.

  3. You may be trying to publish a library, which is not supported. Use dotnet pack to distribute libraries.

Can not find runtime target for framework ‘.NETCoreApp,Version=v1.1’ compatible with one of the target runtimes: ‘win10-x64, win81-x64, win8-x64, win7-x64’.

How to fix it

Open your project.json and under “dependencies” change

"Microsoft.NETCore.App": "1.1.0",

to

"Microsoft.NETCore.App": { "version": "1.1.0", "type":  "platform" },

Then re-run your app.

 

How it happened

I’m still using Visual Studio 2015, but I have .NET Core 1.1 downloaded and installed and I wanted to upgrade to .NET Core 1.1 and update to all the latest ASP.NET Core 1.1 packages.

I went to File => New Project => ASP.NET Core Web Application, which proceeded to give me .NET Core 1.0.1, which was expected.  I followed the upgrade path, but I forgot to hit “Save” on my project.json in step 2, when I changed the values to .NET Core 1.1.0.  Therefore, it did not restore the .NET Core 1.1 NuGet package.

So when I went to the NuGet UI under Updates, .NET Core was an option to update, which I didn’t realize when I hit “Update All.”  Well that jacked with my project.json in this way…

Before:

core101

After:

netcoreapp11

NuGet removed my JSON nested object with just the version number.  What the “type”:”platform” does is it tells my project that it expects .NET Core will already be installed on the machine, therefore it will not bundle .NET Core with my app.

When “type” : “platform” is NOT specified, then it assumes I am bundling .NET Core WITH my app.  In that case, I need to specify the exact platforms that I’m targeting (i.e. Windows 10, Linux, Mac, etc.) in the “runtimes” section.  However, that “runtimes” section was not there previously, because I was targeting the platform.

This seems like it would be a bug with NuGet.  It should only update the Version, and not overwrite anything else.  Maybe this gets fixed during the switch to csproj coming soon.  I’m not going to file a bug at this point on GitHub, because I doubt they make the investment to fix this in project.json.

Glyphfriend lost font intellisense fix

I use the Glyphfriend extension which gives you intellisense for a bunch of font based libraries (such as Font Awesome, Glyphicons and others) and it gives you preview of the icon as you type.  I highly recommend it.  It looks like this:

fontawesome

This is extremely helpful, and allows me to avoid going to FontAwesome’s site to search for an icon I need, but for some reason it went away in one of my projects, and all I got was the font awesome icons I’ve used throughout the project, which looked like this:

fontawesomebroken

The Solution

Turns out someone deleted the non-minified file from the project in an effort to clean up the project.  Adding the full non-minified file back to the project brought my Glyphfriend intellisense preview back and all was right with the world.  Hope this helps someone else.

 

UPDATE: 1/18/2017 – It looks like with Glyphfriend 2.0 this no longer happens.  They acknowledged this limitation  (#1 under “limitations” in this post) and “fixed” this where the icons are compiled at build time and not evaluated at runtime.  Looks awesome.

Feature Folder Structure in ASP.NET Core

A feature folder structure is organizing your app by feature as opposed to technology (Controllers, Views, etc.), which is the default structure in MVC.  A feature folder structure is easier to maintain than the default structure and even Areas (which is a poor man’s feature folder structure).  It looks something like this:

View post on imgur.com

Why would you do this?

Anyone who has built any ASP.NET MVC app of any size often runs into the same problem.  You’re implementing a feature, and you end up spending a lot of time bouncing between folders while you’re implementing the feature.  You keep opening and closing the Controllers folder, the Views folder, the View Models (or Models) folder, the Scripts folder, the Content/CSS folder, the Shared folder under Views, etc.  As your app grows, there are more and more files in these various folders that you have to wade through to find the files you actually need.  All the while, you get the sense you’re fighting your folder structure and fighting the default framework conventions.

Wouldn’t it be nice for all these things that make up the feature to live in the same folder?  That way, you’re just living in one folder (on the front end anyways, business logic and data access logic is likely in another project), and everything is laid out nicely next to each other.  I’ll show you exactly how to do that in ASP.NET Core.  Instead of “feature” you could use the word component, module, etc., but I like the word feature best in this scenario.  I actually like the word component better as a more consistent descriptor across other web stacks (such as Angular, React, etc.), but because View Components are a new feature of ASP.NET Core, the name “component” might be a bit misleading in ASP.NET Core land.  Naming is hard.

I’m not alone in liking this structure

This is not a unique idea.  Not even close.  Jimmy Bogard does it.  Tim Thomas wrote a blog post on it.  Matthew Renze wrote a blog post about doing it in ASP.NET 4.x.  Even other stack experts like Angular guru John Papa structures his apps this way.  And there are many, many other people who like this structure better, even if it goes against the default conventions that Microsoft expects.

If a ton of people do this, why write this blog post?

Because I haven’t seen an implementation of this for ASP.NET Core.

Ok, so how do we do this?

To do this, we simply need to do move some files around and change some configuration about Views.  JavaScript, CSS, ViewModels, etc. are not things known by MVC’s conventions, so we don’t have to worry about those, and Controllers are automatically picked up.

1. Rename the Views folder to Features

2. Add a custom ViewLocationExpander to tell ASP.NET where to find the Views

a. Create a folder to house this config.  I usually call it Startup Customizations or Infrastructure and place it under the root of your csproj, but you can call it whatever you’d like.

b. Create a new file under this folder called FeatureLocationExpander.cs under the folder from the step above and copy the code at this gist (shown below):


using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;
namespace FeatureFolderStructureDemo.StartupCustomizations
{
public class FeatureLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
// Don't need anything here, but required by the interface
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// The old locations are /Views/{1}/{0}.cshtml and /Views/Shared/{0}.cshtml where {1} is the controller and {0} is the name of the View
// Replace /Views with /Features
return new string[] {"/Features/{1}/{0}.cshtml", "/Features/Shared/{0}.cshtml"};
}
}
}

3. Wire up the FeatureLocationExpander in Startup.cs

a. Add the code at this gist (shown below) to your ConfigureServices method in your Startup.cs after the .AddMvc()


// Include this after .AddMvc under ConfigureServices in Startup.cs
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new FeatureLocationExpander());
});

4. Move any feature-specific View Models, JS, and CSS to their respective feature folders.

5. Move the Controllers into their respective feature folders and delete the Controllers folder

6. That’s it!

The only other thing you probably want to do is run your CSS and JS through some sort of task runner (to bundle, minify, and copy to wwwroot), whether that task runner’s Gulp, Grunt, or just plain ol’ NPM scripts which seems to be what the cool kids are doing these days.  There are plenty of other blog posts that explain how to do that, including on the official ASP.NET Core documentation.

Final Thoughts

This leaves us with a much easier to maintain folder structure that allows us to stay focused and productive while writing code, and not wasting time hunting for files in different folders.

 

UPDATE 8/24/2017: For Resharper users – click here to figure out how to fix Resharper’s intellisense.  Thanks again to Bill Sorensen in the comments below who first pointed out this solution to me.