I use Disqus to add commenting feature for my blog posts. Disqus is ridiculously easy to setup:
- Create a new account
- Setup a new site in Disqus
- If not using one of the listed platforms (e.g. WordPress), drop the universal code on your page.
- (optional) Follow documentation on how to pass customized data for each page (why?)
I did the same and I ended up with something like this in my MVC view (script from Disqus and my customizations added in C#):
<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
this.page.url = "@Context.Request.Scheme://@Context.Request.Host@Context.Request.Path";
this.page.identifier = "@Context.Request.Scheme://@Context.Request.Host@Context.Request.Path";
};
(function () { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = '//arminkarimi.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<script id="dsq-count-scr" src="//arminkarimi.disqus.com/count.js" async></script>
Now this is working, and it really took me 10 minutes to set it up. But two weeks later, I just can't live with it. Here are my problems:
- There is too much Disqus code in my view. I rather my view focus on my business requirements (e.g., display the blog post and its bells and whistles), but when I look at my view, I see a lot of Disqus integration distracting me from what's the main focus.
- I am lucky to have only one view in my blog which needs to display Disqus comments. What if I want to reuse this code elsewhere?
- And a less obvious problem (specially with Disqus), what if I am forced to change Disqus (read third party) configurations (change arminkarimi.disques.com to some other domain, because it has changed for any reason)?
Luckily, ASP.NET MVC can easily accommodate these problems by using Partial Views or the newly introduced feature in .NET Core, View Components. There is a lot of argument around using View Components over Partial Views. But, separation-of-concerns and testability benefits which come with View Components makes it a no brainer choice for me.
Here is how I implemented my view component. First, I need a model to pass around Disqus configurations. From Disqus documentation, I extracted possible configuration options.
public class DisqusViewModel
{
public string ShortName { get; set; }
public string PageUrl { get; set; }
public string PageIdentifier { get; set; }
public string PageTitle { get; set; }
public string PageCategoryId { get; set; }
}
I am going to read Disqus short name from an appsettings.json file (click for details).
-
I create an option class:
public class DisqusOptions { public string ShortName { get; set; } }
-
Create the setting section in appsettings.json:
{ // removed for brevity ..., "Disqus": { "ShortName": "arminkarimi" } }
-
Register the configuration in Startup.cs:
public void ConfigureServices(IServiceCollection services) { ... services.Configure<DisqusOptions>(Configuration.GetSection("Disqus")); }
Configurations out of the way, here is DisqusViewComponent.cs for Disqus view component:
public class DisqusViewComponent : ViewComponent
{
private readonly DisqusOptions _config;
public DisqusViewComponent(IOptions<DisqusOptions> config)
{
_config = config.Value;
}
public async Task<IViewComponentResult> InvokeAsync(DisqusViewModel disqusOptions)
{
await Task.Run(() =>
{
if (!string.IsNullOrEmpty(_config.ShortName))
{
disqusOptions.ShortName = _config.ShortName;
}
if (string.IsNullOrEmpty(disqusOptions.PageUrl))
{
disqusOptions.PageUrl = $"{Request.Scheme}://{Request.Host}{Request.Path}";
}
if (string.IsNullOrEmpty(disqusOptions.PageIdentifier))
{
disqusOptions.PageIdentifier = $"{Request.Scheme}://{Request.Host}{Request.Path}";
}
});
return View(disqusOptions);
}
}
The constructor uses the DisqusOptions configuration we have setup so far to get the short name from the appsettings.json file. The InvokeAsync method simply takes the current request URL and assigns it to both PageUrl and PageIdentifier configurations for Disqus. Notice that I preserve any default configuration values so I can let some of my views pass custom options to the view component. Finally we pass disqusOptions to the view component view.
Note that InvokeAsync is an asynchronous method. Therefore, I have created a work to be executed asynchronously to be able to properly implement the method.
Next, let's look at the code for the view (~/Views/Shared/ Components/Disqus/Default.cshtml):
@model arminkarimi.ViewComponents.DisqusViewModel
<div id="disqus_thread"></div>
<script>
var disqus_config = function() {
var pageUrl = "@Model.PageUrl";
if (pageUrl.length > 0) {
this.page.url = pageUrl;
}
var pageIdentifier = "@Model.PageIdentifier";
if (pageIdentifier.length > 0) {
this.page.identifier = pageIdentifier;
}
var pageTitle = "@Model.PageTitle";
if (pageTitle.length > 0) {
this.page.title = pageTitle;
}
var pageCategoryId = "@Model.PageCategoryId";
if (pageCategoryId.length > 0) {
this.page.category_id = pageCategoryId;
}
};
(function() {
var d = document, s = d.createElement('script');
s.src = '//@(Model.ShortName).disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<script id="dsq-count-scr" src="//@(Model.ShortName).disqus.com/count.js" async></script>
Nothing fancy here. I just added some custom code to the original Disqus snippet to make sure Disqus configurations are set only when they are provided. It couldn't get easier at this point. All I have to do is to invoke the Disqus view component and optionally pass custom configuration parameters to it.
<div class="row">
<!--... removed for bravity-->
<div class="col-md-12">
@await Component.InvokeAsync("Disqus", new DisqusViewModel() { PageTitle = Model.Title })
</div>
</div>
If you like to have your view components inside a separate class library to reuse across your projects, Dave Paquette has an excellent post on how you can do this.