Agenda
Introduction
In a very few words, sBlog.Net is a minimalistic blog engine. This
project was heavily inspired by wordpress, though there are a lot more
to come in the following days! You will be able to do all of the normal
activities that you could do in a blog like adding a post or page,
adding categories or tags, adding additional authors and many more. If I
have to describe sBlog.Net in a single sentence, I would say, “For the
love of asp.net mvc and wordpress”! I could already foresee a lot of why
reinvent the wheel questions. But this is just an attempt at something
really simplistic, which gives the ability for others to easily modify
and play with the code, instead of using a massive system without
understanding the inner working. Also refer to the faq for my attempt at
explaining why reinvent the wheel! I am pretty sure, it would take very
little to modify and extend this blog engine to something you need
within a few hours! To get started, you just need an instance of Visual
Studio 2010 with ASP.Net MVC 3, MS SQL server (express should also be
fine) and optionally IIS.
Before I perform a deep-dive inside the code, let me first give you a
series of steps to get started with setting up a development environment
and a test environment, because, getting an idea of how the blog engine
looks would definitely be helpful in understanding the discussion
below. In case you want to look at an online demo, check it out
here! Also,
this is the project page, where you could find more screen shots and a brief description of about the project!!!
Setting up the Development Environment
This section quickly lists the steps to get started with the
development version. You need to have Visual Studio 2010 (or 2008/2012),
MS SQL Server (full / express) is required.
- Download the sBlog.Net sources and open it in Visual Studio
-
Launch MS SQL server, create a new database for use by your blog - as an administrator or a user who has db_creator
privileges. If the database was created by the user that is going to be used in the web.config file, skip the following
steps to give db_owner access.
- Then, if you don't already have a login, create a login
- Now, you need to provide access to the newly created database for the user you created in the previous step
- To do this, expand Security, then expand Logins, right click on the user your created, choose Properties
- In the dialog that appears, choose User Mapping
- In the right pane, select the database you created, check the checkbox corresponding to the new database
- In the Database role membership section, choose db_owner, click on OK
-
Now, within the sBlog.Net sources, locate the sBlog.Net.DB project, then drill down to v1.0\Install. You will find a
Init.sql file here
- Copy the contents of the file, paste it to a new query window in
your new ms sql database you created in the previous steps, and execute
it
- Go to visual studio, expand the sBlog.Net project, open the
web.config file and modify the connection string to use the new
database, user you created
- Hit on Ctrl + F5, you will get to the setup screen. Just follow the on-screen instructions from now on!
Setting Up a Test Environment - IIS
This section deals with the part of setting up a test environment. To
use these steps your copy of windows must have IIS enabled with all the
necessary features (IIS can be installed/enabled from the windows
features in the control panel, however, it may vary according to the
type of windows operating system).
- Download the sBlog.Net binaries zip file and extract the files. It will contain 2 folders App and DB.
-
Launch MS SQL server, create a new database for use by your blog - as an administrator or a user who has db_creator
privileges. If the database was created by the user that is going to be used in the web.config file, skip the following
steps to give db_owner access.
- Then, if you don't already have a login, create a login
- Now, you need to provide access to the newly created database for the user you created in the previous step
- To do this, expand Security, then expand Logins, right click on the user your created, choose Properties
- In the dialog that appears, choose User Mapping
- In the right pane, select the database you created, check the checkbox corresponding to the new database
- In the Database role membership section, choose db_owner, click on OK
- Now, within the folder where you extracted the sBlog.Net binaries, drill down to DB folder. You will find a Init.sql file here
- Copy the contents of the file, paste it to a new query window in
your new ms sql database you created in the previous steps, and execute
it
- Now (assuming your IIS directory is C:\inetpub\wwwroot), create a folder called sblog
- Copy the contents of the folder App\sblog to the folder you created in the previous step
- Now, right click on the "Uploads" folder, choose Properties. Then select the "Security" tab
-
Click on "Edit", if you do not find IIS_IUSRS add it. Then select the user, give "Full Control" for the
Uploads folder, click on OK in the dialogs opened
- Then, open the web.config file and modify the connection string to use the new database, user you created
- Now open IIS manager, from the Start menu or by entering inetmgr in your Run dialog
- Create a new website by right clicking on the Sites node and choosing Add Web Site
-
In the Add Web Site dialog,
- Enter a site name
- Select ASP.Net 4.0 application pool (or) create a new app pool that uses the ASP.Net 4.0 framework
- In the Physical path text box, choose the folder (sblog), created in the previous step
- Now select OK
Now your website is all set to be used
- To launch the web site, right click on it, choose "Manage Web Site" and then "Browse"
Organization of the Project
The sBlog.Net solution consists of 8 projects altogether. I have listed them below with a short description for every item:
- sBlog.Net - This project has the blog engine's core. The framework used is ASP.Net MVC 3
-
sBlog.Net.DB - This project contains the .sql
files that are necessary to setup the database for the sBlog.Net
project. The folders are organized in such a way that installation is
easy. As of now there is a folder called v1.0. Within this there is an Install and an Update. Install folder has a Init.sql
file that can be run on a database to create sBlog.Net related tables.
Update folder does not contain anything for. I also have a plan to
create a windows application that could be used to manage the sBlog.Net
database(s).
- sBlog.Net.Domain - This project contains the
abstract interfaces, their concrete implementations, some extensions and
utilities shared by other projects in the solution
- sBlog.Net.MetaData - The metadata related to
various view models used in the sBlog.Net project is managed here. Also,
the attributes used by the metadata classes are also added to this
project
- sBlog.Net.Akismet - Comment spam is a major issue
for blog engines. So sBlog.Net has Akismet support in-built to validate
comments submitted. However, by default the comments are not validated
using akismet. You will have to enable this from the settings section,
once the blog setup is complete
- MVCSocialHelper - Social sharing is very much
required in order to gain exposure. So sBlog.Net has social sharing
in-built with the help of the open source project http://mvcsocialhelper.codeplex.com/
- sBlog.Net.Tests - Do I even have to explain this project?! Quintessential unit test project!
- sBlog.Net.Tools - This project contains
tools/utilities that will be be used during the pre and post-build
event(s). As of now the project contains a javascript minifier which is
used during the post-build event, where the administration section
javascript files are minified. The minified script is used when the
project is run in the release mode
Saving/Restoring the Settings Related to the Blog
For a project of this scale, it is really necessary to provide the
blog owner ability to update things at ease, without having to modify
the source, as you have to remember this is not just a php application
where you could just go on to the server hosting your application and
modify it (even if it is php, I am not advicing you do this though!).
So, "one size fits all" is not appropriate for a blog engine as it has
to be highly configurable. Thus we need the ability to save and restore
settings related to an instance of the blog. There are number of ways to
do this. You could just have an xml file or a custom file for this. But
this will also reside on the file system and so if the web application
is compromised, it also compromises the settings of the blog. So I
decided to go with the choice of saving the settings in the database.
Another aspect to consider is the fact that this is going to be an
ever-changing table. So a flat table with a column each for a setting
won't work. So I went for a very simple table based on key / value
pairs, so that the changes to this table will be minimal, without having
to add columns infinitely as the project might grow in to a stage where
there are numerous setting items to be stored (as of now there are 19
entries). Given below is the create table statement corresponding to
this table (named
sBlog_Settings
).
Collapse | Copy Code
CREATE TABLE [dbo].[sBlog_Settings](
[KeyName] [varchar](50) NOT NULL,
[KeyValue] [varchar](max) NULL
) ON [PRIMARY]
The administration side has a corresponding settings page where you get
to manage these settings. A screenshot of this page is shown below:
Identifying the Status of the Setup
One of my main intention was to make sure that even people who do not
know anything about ASP.Net to use it. So I wanted the process of
setting up the blog to be as simple as possible. So when they follow the
method to set up using the
binaries,
the on-screen instructions should help them setup the blog without
having to drill down to the code. So, it is important that during the
initial run the blog owner to be able to just follow the on-screen
intructions to setup the blog.
In order to deal with identifying whether the setup for the blog is complete or not, wea may have to use the
Application_Start
and
Session_Start
events that are part of the Global.asax.cs file. If you recall,
Application_Start
method is invoked whenever the application starts for the first time or
when the application restarts. Some activities that can cause an
application to restart are (not limited to):
- Modifications to the
web.config
file
- Recycling the application pool assigned to the website
- Restarting the web site
- Restarting the IIS server itself
Collapse | Copy Code
protected void Application_Start()
{
VerifyInstallation();
}
private void VerifyInstallation()
{
try
{
var settingsRepository = InstanceFactory.CreateSettingsInstance();
var installationStatus = settingsRepository.InstallationComplete;
Application["Installation_Status"] = installationStatus.ToString().ToLower();
}
catch
{
Application["Installation_Status"] = "ERROR";
}
}
If you notice in the above snippet, there is a call to the
VerifyInstallation
method during the
Application_Start
event. This method does a very simple activity - Creates an instance of
the settings repository using the instance factory. Note that this is
in the Global.asax.cs and we do not have the application running fully.
So we cannot use dependency injection here. However, as this instance is
created using the ninject dependency mangement module, disposing off
this instance will also be taken care by it. Once I have an instance, I
attempt to find the status of installation from the sBlog_Settings
table, which is a boolean value field identified by the key name
InstallationStatus.
This method could fail for any number of reasons, such as, invalid
connection string, invalid database name etc etc. Eitherways, the status
of the installation is stored in an application variable called
Installation_Status. More discussion about dependency injection follows next!
Now that we have seen how the application status is determined, lets
now see what happens once the application is started. After tha
application is started, among other activities that takes place, we are
more worried about when a session starts. This happens when a user has
accessed the website. Whenever a new session is started, the application
variable I discussed earlier is checked to make sure the user is
redirected to a setup page, instead of loading the application, as shown
below:
Collapse | Copy Code
protected void Session_Start()
{
var installationStatus = Application["Installation_Status"];
var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
if (installationStatus != null && installationStatus.ToString() == "false")
{
Response.Redirect(urlHelper.RouteUrl("SetupIndex"), true);
}
if (installationStatus != null && installationStatus.ToString() == "ERROR")
{
Response.Redirect(urlHelper.RouteUrl("SetupError"), true);
}
}
The setup consists of 2 steps. In the first step, to make sure you are
the owner, you are asked to enter the connection string for the
application. Once it is verified, you get to see the
Continue >>
button to move on to step 2. Note that the text box to enter the
connection string and the button do not appear until 2 criteria are
satisified - (1) The connection string in the database should be valid
(2) The
Uploads folder should be "writeable", i.e.,
IIS_IUSRS
user should have full access to this folder (and nothing else!). In the
second page of the setup, you get to choose the blog's name, root url
and the administrator password (default user name is
admin
).
Dependency Injection
Managing connections is an integral part in dealing with an application
that has anything and everything to do with databases. So managing them
yourself may not be advisable. Adding a dependency injection framework
definitely helps, as the process of disposing of instances created are
managed by itself. sBlog.Net project uses NInject for managing
dependencies. This facilitates loose coupling between various modules,
which enables you to replace the modules independent of each other. The
NinjectControllerFactory
class takes care of the process by which a controller instance gets the required dependencies. This class extends the
DefaultControllerFactory
class provided by the mvc framework.
Application_Start
method is used to setup ninject as shown below:
Collapse | Copy Code
protected void Application_Start()
{
SetupDependencyManagement();
}
private void SetupDependencyManagement()
{
var ninjectControllerFactory = new NinjectControllerFactory();
ControllerBuilder.Current.SetControllerFactory(ninjectControllerFactory);
DependencyResolver.SetResolver(new NinjectDependencyResolver(ninjectControllerFactory.GetKernel()));
}
The
SetupDependencyManagement
method handles the process
of setting up ninject. The first step is to create the ninject
controller factory as discussed earlier. Then this instance passed on to
the
SetControllerFactory
method of the current
ControllerBuilder
object. In the next step, the
GetKernel
method of
NinjectControllerFactory
is used to get a reference to the kernel in order to create a dependency resolver using the
NinjectDependencyResolver
class, which is then passed to the
SetResolver
. I will elaborate on dependency resolver towards the end of this section!
The
GetControllerInstance
method in order to return an instance of the required controller. Given below is the method:
Collapse | Copy Code
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
try
{
var defaultController = base.GetControllerInstance(requestContext, null);
return defaultController;
}
catch (HttpException httpException)
{
if (httpException.GetHttpCode() == (int) HttpStatusCode.NotFound)
throw new UrlNotFoundException("Unable to find a controller");
throw;
}
}
return (IController)_kernel.Get(controllerType);
}
In line 3, if the framework was unable to create a controller instance, it passes a
null
.
If not, NInject takes the responsibility of creating an instance of the
controller by passing in the required dependencies. Apart from the
GetControllerInstance
method, there is a private class that is used as a "kernel" by the
ninject module. The first line of the class creates a kernel as shown
below:
Collapse | Copy Code
private readonly IKernel _kernel = new StandardKernel(new ApplicationIocServices());
The kernel above will be used by the ninject module to find/create instances based on the dependency required. The
ApplicationIocServices
class is the one that serves as the provider of a binding between an
abstract instance and a concrete instance. inherit's from the
NinjectModule
class and implements the
Load
method. This is the method where an interface is bound to a concrete class.
Collapse | Copy Code
private class ApplicationIocServices : NinjectModule
{
public override void Load()
{
Bind<IUser>().To<User>();
Bind<IPost>().To<Post>();
}
}
In the above snippet, you can observe that the interfaces
IUser
and
IPost
are bound to the concrete entities
User
and
Post
. These dependencies in the form of an interface can be used as shown below, in the
HomeController
.
Collapse | Copy Code
public class HomeController : BlogController
{
private readonly int _postsPerPage;
private readonly IPost _postRepository;
private readonly IUser _userRepository;
private readonly ICategory _categoryRepository;
private readonly ITag _tagRepository;
private readonly ICacheService _cacheService;
public HomeController(IPost postRepository, IUser userRepository, ICategory categoryRepository, ITag tagRepository, ISettings settingsRepository, ICacheService cacheService)
: base (settingsRepository)
{
_postRepository = postRepository;
_userRepository = userRepository;
_categoryRepository = categoryRepository;
_tagRepository = tagRepository;
_postsPerPage = settingsRepository.BlogPostsPerPage;
_cacheService = cacheService;
}
}
Every interface such as
IPost
,
IUser
etc. also forces the user to implement the
IDisposable
interface, thereby safely disposing the database connections. When NInject disposes an instance it created, it also calls the
Dispose
method, thereby automatically disposing the sql connections created. Here is the definition for the
IUser
interface.
Collapse | Copy Code
public interface IUser : IDisposable
{
UserEntity GetUserObjByUserID(int userID);
UserEntity GetUserObjByUserName(string userName, string passWord);
}
I know there has been a lot of discussion about DI, but there is one
more thing I need to point out as promised in the previous section. As
far as I have to inject the dependencies to a controller, I am in a good
shape as ninject takes care of that automatically once I set the
controller factory for the application. But I also have to consider
situations where ninject cannot inject the dependencies automatically.
An instance is the html helpers where ninject has no control. So, at
these scenarios I use a dependency resolver. Earlier, I introduced you
to the method which binds an abstract type to a concrete type. So
ninject already knows about all of the bindings. So, when you request a
service by passing in a abstract type, it will be able to return an
instance of the concrete type. To enable ninject to do this, I have
created a dependency resolver called
NinjectDependencyResolver
that implements the
IDependencyResolver
interface provided by ninject, as given below:
Collapse | Copy Code
public class NinjectDependencyResolver : IDependencyResolver
{
private readonly IKernel _kernel;
public NinjectDependencyResolver(IKernel kernel)
{
_kernel = kernel;
}
public object GetService(Type serviceType)
{
return _kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return _kernel.GetAll(serviceType);
}
}
I guess you remember the line below, which informs ninject that it should use
NinjectDependencyResolver
as the dependency resolver:
Collapse | Copy Code
DependencyResolver.SetResolver(new NinjectDependencyResolver(ninjectControllerFactory.GetKernel()));
After this step, I have the ability to get a dependency anytime and
anywhere even if the class is not a controller class. Consider a section
of the
GetCommonScriptsAndStyles
html extension to get the scripts and styles for the blog.
Collapse | Copy Code
public static MvcHtmlString GetCommonScriptsAndStyles(this HtmlHelper htmlHelper)
{
var settingsRepository = InstanceFactory.CreateSettingsInstance();
}
Notice line 2, where I get an instance of the settings repository using the
CreateSettingsInstance
method in the
InstanceFactory
class. Given below is the content of this method:
Collapse | Copy Code
public static ISettings CreateSettingsInstance()
{
return DependencyResolver.Current.GetService<ISettings>();
}
An instance of the concrete type for
ISettings
abstract type is created in this method using the
GetService
method which is a method provided by the current instance of the
application's dependency resolver. Thus, instead of adding a method to
the
NinjectControllerFactory
class, and using the kernel to
create an instance, I just let ninject itself to handle the process of
creating instances for classes other than the controller.
Ability to Cache Data to Reduce the Number of Database Calls
Let us consider the scenario where a user browses to our blog engine.
By default, the blog engine picks the first 5 posts and displays it. The
user now has the option to either view the previous 5 posts or click on
an individual post or filter posts by categories/ tags/ month &
year combination. Irrespective of the type, note that we have to go to
the database every time to get the posts. In order to avoid this and
make the blog more responsive, I have added the ability to get the posts
from the cache. By default the cache duration is set to 5 minutes in
the
ApplicationConfiguration
class as shown below:
Collapse | Copy Code
private const int DefaultCacheDuration = 5;
public static int CacheDuration
{
get
{
var cacheDuration = ConfigurationManager.AppSettings["CacheDuration"];
int parsedDuration;
return int.TryParse(cacheDuration, out parsedDuration) ? parsedDuration : DefaultCacheDuration;
}
}
Note the property
CacheDuration
which is used by the application to get the cache duration. In the first line, I attempt to get the cache duration from
<appSettings />
section, which is the value corresponding to the
CacheDuration
key.
Collapse | Copy Code
<appSettings>
// -- Snip --
<add key="CacheDuration" value="5"/>
</appSettings>
In the last step, I try to parse the value as an integer. If at all it
fails, I return the default cache duration of 5. Let us now see how this
cache duration is put in to use. If you recall, I earlier discussed
about the
ApplicationIocServices.Load
method that bound an abstract instance to a concrete instance. This method also binds the
ICacheService
type to the
CacheService
concrete type as shown below:
Collapse | Copy Code
private class ApplicationIocServices : NinjectModule
{
public override void Load()
{
Bind<ICacheService>().To<CacheService>();
}
}
This dependency is requested only in the controllers that would use
them. For instance, controllers in the administration section does not
need the cache service, because once the user is logged in, he/she
should be able to view up to date content. Given below is a part of the
home controller.
Collapse | Copy Code
public class HomeController : BlogController
{
private readonly int _postsPerPage;
private readonly IPost _postRepository;
private readonly IUser _userRepository;
private readonly ICategory _categoryRepository;
private readonly ITag _tagRepository;
private readonly ICacheService _cacheService;
public HomeController(IPost postRepository, IUser userRepository, ICategory categoryRepository, ITag tagRepository, ISettings settingsRepository, ICacheService cacheService)
: base (settingsRepository)
{
}
private List<PostEntity> GetPostsInternal()
{
var posts = Request.IsAuthenticated ? GetProcessedPosts(_postRepository.GetPosts(GetUserId())) : _cacheService.GetPostsFromCache(_postRepository, CachePostsUnauthKey);
return posts;
}
}
Notice the constructor - it gets an
ICacheService
instance which is used in the
GetPostsInternal
method.
GetPostsFromCache
is a fluent extension for the
ICacheService
interface. It is given below:
Collapse | Copy Code
public static List<PostEntity> GetPostsFromCache(this ICacheService cacheService, IPost postRepository, string keyName)
{
return cacheService.Get(keyName, () => postRepository.GetPosts());
}
This method takes in a Post repository and a key name, while being
invoked with an instance of cache service. This method invokes the
Get
method by passing in the keyname and a callback for the method to
execute if the cahce does not contain this item identified by the key
passed. It will be clear once I introduce you to the
Get
method.
Collapse | Copy Code
public class CacheService : ICacheService
{
public T Get<t>(string cacheID, Func<t> getItemCallback) where T : class
{
var item = HttpRuntime.Cache.Get(cacheID) as T;
if (item == null)
{
item = getItemCallback();
HttpContext.Current.Cache.Insert(cacheID,item,null,DateTime.Now.AddMinutes(ApplicationConfiguration.CacheDuration),Cache.NoSlidingExpiration);
}
return item;
}
}
</t>
In the previous paragraph, I discussed about the callback that the
method would have to use to update the cache if the data is not
available. Non-availability of data happens either during the initial
launch of the application or when the cache duration expires. This is
where the
CacheDuration
property comes in to the picture.
Let us drill in to the method even more for me to explain this. The
first line in this method first attempts to get the data in the cache by
using the
HttpRuntime.Cache.Get
method and casts it to the generic type parameter
T
.
If the item is null, the callback passed is used to get the data and is
first inserted in to the cache. Then the data is returned to the user.
Notice the 4th parameter which uses the cahce duration we discussed
earlier. This is a fixed cache duration, after which the contents of the
cache identified by the key passed is invalidated. Last parameter is
used to pass a sliding duration, which implies that the duration passed
will renew every time the data from the cache is accessed. So assuming
it is set to 10, when the data is accessed for the first time the cache
item is created. Then after 5 minutes, if the cache item is accessed,
the duration is again set to 10 minutes and so on.
You may think why I chose to have the cache duration key in the
web.config file instead of the settings, thereby providing the ability
to update the cache time from administration pages. Before I get in to
that, I am sure you know that an application will be restarted if you
make changes to the web.config file corresponding to that application by
this time. This in turn clears out all the items in the cache, clears
out application variables, session variables and so on. This is one
reason I wanted to have the cache duration key in the web.config file,
thereby clearing out the cache whenever you update it.
Authenticating the Users
This project uses a custom membership provider in order to assume
complete control of how a user is authenticated. The first step in
implementing a custom membership provider is to create a class that
extends the MembershipProvider class. This class has a number of
methods, but at this point, the emphasis is on few methods and
properties - create a user, get a user entity by passing the username,
minimum password length, whether a unique email is required and
validating a user. Then you have to modify the
web.config
to inform the mvc framework to use the custom membership provider to deal with logins and logouts. The sBlog.Net's
web.config
file already has the custom membership element added with the help of
the <membership /> element. Note the fully qualified name in the
type
attribute, and the
connectionStringName
attribute.
Collapse | Copy Code
<membership defaultProvider="CustomMembershipProvider">
<providers>
<clear/>
<add name="CustomMembershipProvider" type="sBlog.Net.Infrastructure.CustomMembershipProvider"
connectionStringName="AppDb"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
applicationName="/" />
</providers>
</membership>
This
article of mine discusses more about custom membership providers. Feel
free to have a look at this as adding that content over here would make
this article too long, that you would give up reading it
The Base Controller
All the controllers in this project inherits from the
BlogController
which inherits from the
Controller
class, which is what all the controllers should inherit, in order to be
usable as a "controller". This base controller provides number of
properties and methods that will be shared across other controllers in
the project. It also does the most important part of deciding which
layout page to use depending on the theme chosen by the user. Consider
the
OnActionExecuted
method given below:
Collapse | Copy Code
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
var action = filterContext.Result as ViewResult;
if (action != null && !string.IsNullOrEmpty(ExpectedMasterName))
{
var themeName = SettingsRepository.BlogTheme;
if (ThemeExists(themeName))
{
action.MasterName = MasterExists(themeName) ? string.Format(LayoutFormat, themeName, ExpectedMasterName)
: string.Format(DefaultLayoutFormat, ExpectedMasterName);
}
else
{
throw new InvalidThemeException("Invalid theme {0}", themeName);
}
}
base.OnActionExecuted(filterContext);
}
The base controller has a property called
ExpectedMasterName
.
This property is set only in the controllers that serve the main blog.
The admin section controllers do not set this property. So this method
first checks if this property is set. If so, it gets the blog's theme
from the settings and checks if the theme exists - whether a folder with
the specified name exists in the "Themes" folder. Then, it checks
whether the theme folder contains a layout file. If so, that layout file
is used. Otherwise, the common layout available in the root's Views
folder is used. If the
ExpectedMasterName
is set, but the theme folder does not exist, the application throws an exception and halts.
Let me also discuss a few more methods that are integral to the project. Given below is the
GetUserId
method:
Collapse | Copy Code
protected int GetUserId()
{
var userId = -1;
if (Request.IsAuthenticated)
{
var userIdentity = (IUserInfo)User.Identity;
userId = Int32.Parse(userIdentity.UserId);
}
return userId;
}
This method returns the user id for the user logged in. If nobody is logged in it just returns -1. Every controller exposes a
User
property, which implements the
IPrincipal
interface and this object exposes a property called
IIdentity
. This object hsa a lot of useful information that could be used, if the current user is logged in. The
User
object is initialized after a user is authenticated in the Global.asax.cs file during the
PostAuthenticateRequest
event, as given below:
Collapse | Copy Code
public override void Init()
{
PostAuthenticateRequest += MvcApplication_PostAuthenticateRequest;
base.Init();
}
void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
var encTicket = authCookie.Value;
if (!String.IsNullOrEmpty(encTicket))
{
var ticket = FormsAuthentication.Decrypt(encTicket);
var id = new UserIdentity(ticket);
var prin = new GenericPrincipal(id, null);
HttpContext.Current.User = prin;
}
}
}
Note lines 16 - 18 in the code snippet above. Once a user is
successfully authenticated, this method first verifies the
authentication cookie, and then creates an instance of
UserIdentity
, which implements the
IIdentity
(required) interface provided by the framework and also I have created an interface called
IUserInfo
, which has 2 properties -
UserId
and
UserToken
. These 2 properties are used across the application to get information related to the user. The usage of
UserId
should be immediately obvious to you. But,
UserToken
may not be. In sBlog.Net, a number of activities are carried out using
ajax requests. In order to safely serve these requests, apart from
checking if the request is authenticated, I also validate the token
passed from the client side. The request is served only if the user is
authenticated and the user's token matches the token of the user in the
database. This token is set/reset during every login and is unique for a
particular user. Given below is a section of the code in the manage
comments view. Whenever a page has to issue ajax requests, this page
also has to pass the one time token for the user. So it is rendered as
shown below:
Collapse | Copy Code
@Html.HiddenFor(model => Model.OneTimeCode)
Once the token is rendered in the view, it can be used as shown below.
This is part where an ajax request is performed to delete a comment.
Line 9 is of prime interest. Data passed to the url also includes the
one time token along with the id of the element to be deleted.
Collapse | Copy Code
$('.trashComment').click(function (e) {
e.preventDefault();
var anchor = this;
var commentId = $(anchor).next('.trashCommentID').val();
$.ajax({
type: 'GET',
url: siteRoot + 'Admin/CommentAdmin/TrashComment',
data: { 'commentId': parseInt(commentId), 'token': $('#OneTimeCode').val() },
dataType: 'json',
success: function (data) {
if (data.DeleteStatusString == "Delete succeeded") {
var row = $(anchor).parent().parent().parent();
$(row).remove();
}
},
error: function (req, status, err) {
alert('an error occurred while trying to delete the selected comment, please try again.');
}
});
});
All of this discussion was to give you a background for the following
method, which provides a way for the controllers to get the one time
token for the user logged in.
Collapse | Copy Code
protected string GetToken()
{
var userIdentity = (UserIdentity)User.Identity;
return userIdentity.UserToken;
}
Replacing The Default String Hashing Mechanism
Now that we have discussed about how a user is authenticated, let us now see how it can be customized to our needs. The
web.config
has a very important key
hasher
, which decides how your strings are hashed. The value for this key by default is
sBlog.Net.Domain.Hashers.Md5Hasher
which is the fully qualified name for a class that implements the
IHasher
interface. Here is the interface definition:
Collapse | Copy Code
public interface IHasher
{
string HashString(string srcString);
}
The default hasher,
Md5Hasher
just returns the calculated
md5 hash of the string entered using a utility class already available
in the project, as shown below. Note that it implements the
IHasher
interface.
Collapse | Copy Code
public class Md5Hasher : IHasher
{
public string HashString(string srcString)
{
return HashExtensions.GetMD5Hash(srcString);
}
}
As you all might have heard, MD5 is not the most safest way to hash a
string. So, I would always advice replacing this with a more secure
hasher, such as SHA-1. In order to replace the default hasher, first you
have to create a new hasher that implements the
IHasher
interface. For example, create a new class
ShaHasher
under the same namespace as the
Md5Hasher
. Then implement the
IHasher
interface and implement the method to hash a string passed. In order to instruct the blog engine to use this, update the
web.config
key to the fully qualified name of the new type you created, say,
sBlog.Net.Domain.Hashers.ShaHasher
.
Few Words About the Password Helper
The
PasswordHelper
class uses this hasher to hash
passwords which are combined with the user code's, so that the username
and passwords will not be vulnerable to dictionary attacks. Let us
divert our attention to the
PasswordHelper
class for a few minutes. The class definition is given below:
Collapse | Copy Code
public static class PasswordHelper
{
public static string GenerateHashedPassword(string userPassword, string randomCode)
{
var hasher = Hasher.Instance;
var hashedPassword = hasher.HashString(string.Format("{0}{1}", userPassword, randomCode));
return hashedPassword;
}
}
sBlog.Net hashes passwords along with a salt, which is generated by the
blog engine itself and is stored in the database along with other
information for a user. As an additional layer of security, the user
code is encrypted in the database. However, the passwords are
hashed and not
encrypted, as that's the recommended way to store a password. The
GenerateHashedPassword
method thus takes in the password entered by the user and a
decrypted user code. This method is used whenever there is a need to validate the user name and password entered by a user.
An
important thing to remember is that, once you complete the setup of the
blog, you can no longer change this key as this would cause all
password validations to fail.
Now that we have seen all about updating the hashing mechanism for the
blog, let me briefly get in to how all of this is connected. If you
notice, I never discussed about the following line:
Collapse | Copy Code
var hasher = Hasher.Instance;
Instance
is a static property in the
Hasher
static class. The function of the
Hasher
class is to create an instance of the hasher defined in web.config using the
Hasher key, explained in the previous section. Let us have a look at this class. I find it quite interesting
!
Collapse | Copy Code
public static class Hasher
{
private const string DefaultHasher = "sBlog.Net.Domain.Hashers.Md5Hasher";
public static IHasher Instance
{
get
{
IHasher iHasher;
var assemblyName = Assembly.Load("sBlog.Net.Domain").CodeBase;
try
{
var hasher = ApplicationConfiguration.HasherTypeName;
iHasher = (IHasher)Activator.CreateInstanceFrom(assemblyName, hasher).Unwrap();
}
catch
{
var instance = Activator.CreateInstanceFrom(assemblyName, DefaultHasher).Unwrap();
iHasher = (IHasher)instance;
}
return iHasher;
}
}
}
ApplicationConfiguration
, as the name implies contains properties that could be shared across the application. It contains a property called
HasherType
which returns the fully qualified type name specified in the web.config file. The
Instance
property first tries to get this value and tries to create an instance of that type. Note that it attempts to load from the
sBlog.Net.Domain assembly only for now. If the process of creating an instance fails, a default md5 hasher instance is created and retruned.
CreateInstanceFrom
method is used to create an instance of
ObjectHandle
by passing the assembly name where this type's metadata could be
identified along with the fully qualified type name of the hasher. This
ObjectHandle is then converted in to form which could be cast in to a
known type using the
Unwrap
method. So, throughout the blog
engine, when hashing is required, we just get an instance from this
static class, so that we don't have to worry about intricate details of
looking through various assemblies and creating instances for our use.
Custom Binding Complex Data Types - Managing Post/Page Add and Edits
Custom model binding is a great feature of mvc 3 that could be used to
bind a complex c# object. In the case of sBlog.Net custom model binding
is used to bind the contents of the data posted to a
PostViewModel
, which also includes a
CheckBoxListViewModel
property. Let us analyze one of them, which is the binder for
PostViewModel
. This is called as the
PostViewModelBinder
and is present in the
Binders folder within the sBlog.Net (core) project.
Collapse | Copy Code
public class PostViewModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var postModel = new PostViewModel
{
Post = new PostEntity {PostID = int.Parse(bindingContext.GetValue("Post.PostID"))
postModel.Post.Order = postModel.Post.EntryType == 2 ? (int?)GetOrder(bindingContext.GetValue("Post.Order")) : null;
IModelBinder ckBinder = new CheckBoxListViewModelBinder();
postModel.Categories = (CheckBoxListViewModel)ckBinder.BindModel(controllerContext, bindingContext);
if (postModel.Post.EntryType == 1)
{
if (!postModel.Categories.Items.Any(c => c.IsChecked))
{
var general = postModel.Categories.Items.SingleOrDefault(c => c.Value == "1");
if (general != null)
{
general.IsChecked = true;
}
}
postModel.Tags = bindingContext.GetValue("hdnAddedTags");
}
return postModel;
}
private static int GetOrder(object value)
{
int parsedValue;
if (value == null || value.ToString() == string.Empty)
return int.MaxValue;
return int.TryParse(value.ToString(), out parsedValue) ? parsedValue : int.MaxValue;
}
}
I don't want to test your patience by explaining every line in detail.
So let me just point out some important points. Note that the class
implements the
IModelBinder
interface and this includes the
BindModel
method. When the mvc framework invokes this method, it passes a
ControllerContext
object and a
ModelBindingContext
object. The
ModelBindingContext
object contains all the fields posted and the subsequent lines attempts to get the values and creates a new
PostViewModel
object. A model binder comes in to the picture when we are attempting
bind a complex model to an object. As far as the types in the parameter
of an action method are simple C# objects, we don't have to worry about
model binders and let the mvc framework take care of it. But in this
case, one look at the model we are binding will prove that it is not
just a simple class that requires primitive binding. Hence the need to
create a custom model binder. Given below is the
Add
method that utilizes model binders.
Collapse | Copy Code
public ActionResult Add()
{
}
[HttpPost]
[ValidateInput(false)]
public ActionResult Add(PostViewModel postModel)
{
}
Note the
PostViewModel
parameter in the
Add
method decorated with the
HttpPost
attribute. Model binders comes in to the picture at this point. When a post request is issued for the
Add
method,
PostViewModelBinder.BindModel
method is called with the necessary parameters. But this method is not
called "magically" and you have to do the necessary changes to make this
happen. This "magic" is done in the Global.asax.cs file as shown below:
Collapse | Copy Code
protected void Application_Start()
{
SetupCustomModelBinders();
}
private void SetupCustomModelBinders()
{
ModelBinders.Binders.Add(typeof(CheckBoxListViewModel), new CheckBoxListViewModelBinder());
ModelBinders.Binders.Add(typeof(PostViewModel), new PostViewModelBinder());
}
In the
Application_Start
method in the Global.asax.cs
file, the binders have to be registered, in order for the framework to
invoke the binders, as shown above. At this point, the first line in the
SetupCustomModelBinders
method that registers the
CheckBoxListViewModel
is not invoked by the framework, but by the
PostViewModelBinder.BindModel
method, as you saw earlier [corresponding line of code is given below]:
Collapse | Copy Code
postModel.Categories = (CheckBoxListViewModel)ckBinder.BindModel(controllerContext, bindingContext);
Even though the model binder for check box list is not called by the
framework, in case you are using a CheckBoxListViewModel in an action
method's parameter, decorated with the
HttpPost
attribute, the framework will call the
BindModel
method of our custom binder.
Saving a Few Cycles
When you install ASP.Net MVC 3, you have the ability to create your
views either using the razor view engine (the default for mvc 3) or the
WebForms view engine (default for mvc 2). If you notice, in this
project, I only use razor views. So, while thinking about the
performance improvements, I read an article on how mvc 3 looks for a
view. Consider a simple action method as given below:
Collapse | Copy Code
public class HomeController
{
public ActionMethod Index()
{
return View();
}
}
When a user requests this method by entering the url
/home/index
,
asp.net mvc looks for the "Index" view. But notice we do not specify
the extension here, that would imply what view engine it
uses. So asp.net mvc will look for the Index view created using the
razor model and the webforms model. In the case of sBlog.Net, it will
find Index.cshtml (razor) and render it. But in order to do this,
asp.net mvc first searches for the Index.aspx view and then search for
the Index.cshtml view. In this case a cycle of the framework is lost
looking for the Index.aspx view. In order to save this cycle, I am
going to instruct asp.net mvc to only consider the razor view engine.
Before I show that code, consider the screen shot below. In this case
the action method returns a view that does not exist and notice
the order in which the file is searched for:
From the above screen shot, you would have noticed that first a webforms
based view is looked up and then a razor view is looked up. Now, let me
introduce you to how I could save a few cycles, with the help of the
code snippet below:
Collapse | Copy Code
protected void Application_Start()
{
SetupViewEngines();
}
private void SetupViewEngines()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
}
In the
Application_Start
method, I call the
SetupViewEngines
method. This method first clears all the engines added in the
Engines
property provided by the framework's
ViewEngines
class (which are the webforms and razor view engines). Then I just add the
RazorViewEngine
alone, thereby saving a few cycles for every action method call, since
asp.net mvc now knows that it need not look for a webforms view when a
view is requested. Here is a screen shot showing what happens when the
above modification is added and a view that does not exist is rendered.
Note that now it is just looking for razor view engine based views!
Generating Unique Slugs
In a blog engine, its extremely critical to have the ability to
generate unique slugs for the url of a post or page, tags and
categories. For urls, not all strings are valid, as these slugs act as a
part of the url. For example,
web.config as a part of
the url is not valid. So we should make sure it does not get to be part
of the url. To make it simple and less prone to errors, before
generating a slug, I first strip of any character that is not one of
a-z, A-Z, 0-9, space, period and the hypen symbol. Let me first show you
some code before I explain in a simple set of steps as to what is going
on:
Collapse | Copy Code
public static string GetUniqueSlug(this string srcString, List<string> allItems)
{
var regex = new Regex(@"[^a-zA-Z 0-9\.\-]+");
var slug = regex.Replace(srcString.ToLower(), string.Empty);
slug = ReplaceMatches(slug, @"[ ]{2,}").Replace(" ", "-");
slug = ReplaceMatches(slug, @"[\.]{2,}").Replace(".", "-");
slug = ReplaceMatches(slug, @"[\-]{2,}");
if (slug.StartsWith("-") && !slug.EndsWith("-"))
slug = string.Format("0{0}", slug);
if (!slug.StartsWith("-") && slug.EndsWith("-"))
slug = string.Format("{0}0", slug);
return allItems.Any(s => s == slug) ? GetUniqueSlugInternal(slug, allItems) : slug;
}
private static string ReplaceMatches(string srcString, string pattern)
{
var removalPattern = new Regex(pattern);
return removalPattern.Replace(srcString, "-");
}
Now that you have seen some code, let me describe very shortly what I am attempting.
- First, I remove any character that is not an alphabet
(lower/upper), number (0-9) and one of space, period, hyphen, along with
converting it to lower case, as case does not matter in urls
- Then I replace 2 or more spaces with a hyphen and a single space with a hyphen
- Then I replace 2 or more periods with a hyphen and a single period with a hyphen
- Then I replace multiple hyphens with a single hyphen
- In the next step, if the slug begins / ends with a hypen (even though not an issue), it is prepended / appended with a "0"
- Finally, if the slug was never used (identified using
allItems
, the existing list of slugs), it is returned as it is. Else an internal method to find a slug in the format <slug>-<number> is called by passing the slug formed so far. Its given below:
Collapse | Copy Code
private static string GetUniqueSlugInternal(string srcString, List<string> srcList)
{
var slugRegex = new Regex(string.Format(@"^{0}-([0-9]+)$", srcString));
var matchingSlugs = new List<int>();
srcList.ForEach(s =>
{
var match = slugRegex.Match(s);
if (match.Success)
{
var number = int.Parse(match.Groups[1].Captures[0].Value);
matchingSlugs.Add(number);
}
});
if (matchingSlugs.Any())
{
var max = matchingSlugs.Max();
return string.Format("{0}-{1}", srcString, max + 1);
}
return string.Format("{0}-2", srcString);
}
I find this method to be quite interesting as I need to find an unused
url. I cannot just use a finite number to generate a slug in the format I
specified before. For example, assume there is already a tag called "f"
corresponding to the tag name "F#". Now if the user enters a new tag
with name "F", this will also boil down to the slug "f", but it is
already used. So we have to find a new slug that does not conflict with
the existing tag. So you will have to start looking for availability in
the order "f-2","f-3","f-4"... and so on. So it's not advisable to have
have a fixed number, say i=99999 and try sequentially. Hence I follow
the approach where I define a regex which identifies any tag starting
with the conflicting slug, followed by a hyphen and then any number of
digits. Then, for all the matches I create a list of numbers and then
find the maximum of it. Adding 1 to this number gives a number that is
never used. In the case of the situation I described before, there won't
be any numbers to look for and this is why I have the default case
where I just return the slug in the format
<slug>-2.
Minifying the JavaScript Files (Admin Section)
With all the amount of ajax calls involved and a number of other
activities, the administration section of this blog engine has a whole
lot of javascript content. Even though browsers do not get the
javascript files everytime, it is very important to reduce the size of
the javascript file that the browser has to save. That's why I thought
automatically minifying the javascript files during the build process is
favorable. Also, it is necessary to use the minified version of the
script files only when the project is running in the release mode. The
current mode under which an application is running under IIS can be
identified using the
<compilation />
element in the web.config file. It's given below:
Collapse | Copy Code
<compilation debug="true" targetFramework="4.0">
</compilation>
Before I get in to how the application itself identifies whether it is
running in debug/release mode, let me discuss about how I handle the
minification process. I use
jsmin.exe (
jsmin.exe by Douglas Crockford)
during the build event. To enable easy minification without any
complicated logic, the scripts are organized in the following fashion.
Within the "Scripts" folder within the
sBlog.Net
project, there is a folder called "Required". Within this folder there
are 2 more folders - "debug" and "minified". The "debug" folder has all
the required scripts with the prefix number denoting the order in which
they should appear in the minified file. When build is carried out, the
minified file is copied on to the "minified" folder. Given below is the
command I use to build the minified script.
Collapse | Copy Code
type "$(ProjectDir)Scripts\Required\debug\*.admin.js" | "$(SolutionDir)sBlog.Net.Tools\Minifiers\jsmin" > "$(ProjectDir)Scripts\Required\minified\script-bundle.min.js"
To find this command in the project, right click on the
sBlog.Net
project, select "Properties", then select the "Build Events" tab and in
the right pane you will find a text area for "Post-build event command
line". This is where this command is entered. This command just lists
the contents of each file, redirects it to the jsmin.exe file. The
jsmin.exe file in turn minifies it and redirects the output to be
appended to the
script-bundle.min.js file. Now let us
get to the next part of this section where how the application
determines the mode it is running under. Even though the
web.config file has the
compilation
element with the
debug
attribute, you don't have to parse the web.config file or do something
fancy to get this information. ASP.Net already takes care of this using
the
IsDebuggingEnabled
property in
HttpContext.Current
as shown below (from AdminScriptProviderHelpers.cs):
Collapse | Copy Code
private static bool IsDebug()
{
return HttpContext.Current.IsDebuggingEnabled;
}
Managing Site Errors
Every error that goes uncaught and unhandled by top level classes are logged in to a table called
Errors
.
The blog owner also has the ability to choose whether or not to receive
emails for every global exception that occurs in the blog engine from
the settings section. Just registering a
HandleErrorAttribute
in
Global.asax.cs
won't be enough in the case of a blog engine. We also have to handle the
Application_Error
method, in order to catch errors that may not be caught by the handle
error attribute, as it happens even before the mvc framework comes in to
the picture. Let me drill down more in to this. Consider the following
snippet:
Collapse | Copy Code
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new BlogErrorAttribute());
}
The
Application_Start
method calls the
RegisterGlobalFilters
in order to register the error filter. This attribute overrides the
OnException
method and logs the exception first. Then depending on various properties the
GetRedirectResultByExceptionType
method decides which error page to redirect the user to. For example,
if the action method belonged to an admin level controller, identified
by the
IsAdminController
property which is set in the
BlogController
base class, part of the
IControllerProperties
interface. However, life is not as easy as you would wish it to be
" /> Errors might happen even before the mvc framework has kicked in. For example, in the
Application_Start
method, or SQL connection errors and so on. That's why I also handle the errors in the
Application_Error
method as shown below:
Collapse | Copy Code
protected void Application_Error()
{
var exception = Server.GetLastError();
var urlHelper = new UrlHelper(HttpContext.Current.Request.RequestContext);
if (exception is UrlNotFoundException)
{
Log(exception);
Response.Redirect(urlHelper.RouteUrl("Error404"), true);
}
else if (exception is SqlException)
{
Response.Redirect(urlHelper.RouteUrl("SetupError"), true);
}
}
Note that if the exception is of type
SqlException
, there
is no point attempting to log it. So we give up logging and just go on
to redirecting the user to a page that informs the user of the issue and
halts execution.
Rich Text Editor to Create Posts/ Pages
Okay, we have seen a lot of "backend" stuff and its time for some
"frontend" stuff, that a normal user or an administrator of the blog
engine views. To begin with let us see a few things on the admin side. I
am going to begin with discussing the most important feature of a blog
engine - a rich text editor to deal with posts/pages). A rich text
editor is an integral part of a blog engine as without one, composing
html based content could become really tedious. I use
CKEditor
for providing this ability. Apart from providing basic ability of
composing html based content, it also provides a number of other
features such as proiving the ability to hook in to features enabling
users to upload files and also provide links in the html content they
produce. Using ckeditor is pretty simple. Following are the steps to
create one:
- Add jQuery to the page
- Add ckeditor.js within the CKEdoitor folder to the page
- Add jquery.js within the CKEditor\adapters folder to the page
- As of now, all the files referred to are added to the _LayoutAdmin.cshtml layout page, that will be used for the administration section
- CKEditor enabled textareas are used across a number of pages - for
adding/editing a post, adding/editing a page. So I created a user
control called AddEditPost.cshtml that will be shared across all of these pages, enabling reuse without repetitivenes.
- This user control consists of a textarea input field for which ckeditor plugin will be applied, with the css class specified as
adminRichText
- Finally the site's admin javascript file applies the plugin as shown below:
Collapse | Copy Code
$(document).ready(function () {
jQuery('.adminRichText').ckeditor();
}
By following these steps a fully functional rich text/html editor can
be created, thereby, enabling the blog authors to easily create html
based posts. Given below is a screenshot of how the add page for a post
looks like:
And given below is a screenshot of how the add page for a page looks like:
What is a blog without the ability for the readers to comment and voice
their opinions? Its equivalent to a book according to my opinion! So,
needless to say, sBlog.Net has commenting included by default!
But a widespread issue blogs face is the unstoppable influx of comment
spam! I took in to account all of this while designing the commenting
system of sBlog.Net. While I was in the ending stages of
designing the blog engine (and the commenting system), I came across
Disqus - a commenting system available for free that can be embedded in
to any site. I decided to include the ability for blog owners to
use disqus in a subsequent release as I was already done with all the
features and have also started testing.
Getting back to the native commenting system of sBlog.Net, it is quite
simple. Every post/page can optionally choose to have a form where users
can enter their comments. It asks for basic information like
the commenter's name, email (optional) and the comment itself. Simple
enough, right? But how do I limit spam without the use of CAPTCHA or
some other fancy tool? I resorted to using 2-tiers of securing the
commenting system, with one being optional. Let me discuss the 1st tier.
The idea is deeply influenced by
Growmap anti spambot
plugin. In this method, apart from the fields specified, a checkbox
is added from the client side using javascript. Thus the checkbox added
is not visible to a spam bot as the checkbox element is not
present in the html generated for the comment form. But a "normal" user
who wishes to comment can see the checkbox and the label (again added
using javascript) associated to this checkbox would instruct the
user to "select" this checkbox to indicate that he is not a
spam bot!
Without selecting this checkbox, the form cannot be submitted and thus a
spam bot that operates based on the html input elements cannot submit
this form at all! But wait, what if the browser has javascript
disabled? Yes, this would bypass this check causing spam to be
submitted. That's why I have added akismet support (turned off by
default). With akismet, a comment can be verified on the server side and
identified whether it qualifies as "ham" (a valid comment) or "spam".
This cannot be bypassed by the spam bot, thereby taking care of comment
spam!
Blog comments can be enabled / disabled site-wide and can also be
disabled on a per post/page basis. This could be useful when you wish to
close comments just for a single post / page (say, the "about"
page). Every author can manage comments posted to his/her posts and the
blog administrator can manage comments site-wide. Refer to the previous
section for screen shots indicating how comments can be
enabled/disabled per post/page.
And, given below are the screenshots of how the comment management page looks for a blog administrator and a "normal" author.
Admin view of other author's comments
Admin view of comments for (admin's) posts
Author's view of comments section for author's posts
Syntax Highlighter & Social Sharing
sBlog.Net has syntax highligheter (by
Alex Gorbatchev) and social sharing (with the help of
MVC Social Helper).
As a developer I always found it helpful by explaining something with
the corresponding code, as it makes the life of the reader easier as
looking in to small snippets of code can really be helpful in
understanding stuff and may also reduce the amount of explanation
required in order convey what you intend. Given below is a post the
contains some C# code.
With the advent of various social networks that enable sharing, I also
thought its crucial to have social sharing available by default. So,
sBlog.Net also has social sharing capability built in. Given below is a
screenshot that shows a page which social sharing icons.
Both syntax highlighter & social sharing can be enabled/disabled
site wide from the settings page. Once either of them is enabled,
authors also control to enable / disable them just for a single post /
page.
Creating a New Theme Using Only Css Files
Now, its time to discuss one of the great feature of sBlog.Net blogging
engine. This and the subsequent section will discuss about creating
themes in order to customize how the blog's layout is. There are a
couple of ways by which you could do this, so, lets start with the
"easy"
way! By following this method you can quickly create a custom designed
theme, instead of using the themes provided by default. This method will
work either when using Visual Studio or IIS. The following are the
steps to create a theme:
- Select the Themes folder
- Right click on the Themes folder, select New Folder, enter a name for your theme
- At a minimum, you should atleast create a css file. So, within your new folder, create a folder called css
- Add a css file, let's name it as style.css
- The layouts used for posts (_Layout.cshtml) and pages (_LayoutPage.cshtml) are located in the ~/View/Shared folder. In your style.css file add all of the css you need.
- Now, save the changes, build / publish your project (if you are doing this from visual studio) and launch it
- Login to the administration section as the administrator and go to the Settings section
- Now, if you select the blog theme drop-down you will notice your new theme
- Select your new theme and save the changes
- You should now be able to see that your theme was applied!
Creating a New Theme Using Layout Files
This section discusses a much more elaborate way by which you can
create a custom designed theme, instead of using the themes
provided by deault. This method will work either when using Visual
Studio or IIS. The following are the steps to create a theme that also
defines the layout:
- Select the Themes folder
- Right click on the Themes folder, select New Folder, enter a name for your theme
- In this method, you have to create the layouts of your post, page and also the css
- Now create 2 files: _Layout.cshtml and _LayoutPage.cshtml
- Create a folder called css, add a file called style.css and add the styles you like
- Refer
to any of the default themes that has these 2 files, add the html to
your new files similar to the ones present in the default themes
- You can also copy/paste existing layout and modify them as per your needs
- Just make sure you have all the sections, like the categories section, recent posts section etc!
- Now, save the changes, build / publish your project (if you are doing this from visual studio) and launch it
- Login to the administration section as the administrator and go to the Settings section
- Now, if you select the blog theme drop-down you will notice your new theme
- Select your new theme and save the changes
- You should now be able to see that your theme was applied!
Finally, let me talk about rss feeds! Rss feeds are an integral part of
any blog engine. It helps users keep track of the status of a blog,
without having to visit the blog every time to check if there are any
new posts. Every major browser supports subscribing to rss feeds and so
its a very important feature in a blog engine. ASP.Net MVC 3 has a
number of
ActionResult
types such as one for returning
html, json, content etc, but does not have one for returning rss data.
The following class satisfies the issue of the missing link! You could
see that it inherits from the
ActionResult
class and overrides the
ExectuteResult
method. Notice the 1st line. This line sets the content type to
"application/rss+xml", as without this, the browser cannot interpret
this page as a rss feed. In the next line an instance of
Rss20FeedFormatter
is created. Using this formatter, the actual content received from the
Output
property of the response object and written to the rss formatter.
Collapse | Copy Code
public class RssActionResult : ActionResult
{
public SyndicationFeed Feed { get; set; }
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.ContentType = "application/rss+xml";
var rssFormatter = new Rss20FeedFormatter(Feed);
using (var writer = XmlWriter.Create(context.HttpContext.Response.Output))
{
rssFormatter.WriteTo(writer);
}
}
}
Let us now see how I am putting this class in to use.
Collapse | Copy Code
public ActionResult Index()
{
var rssFeedViewModel = GetRssFeedViewModel();
var feed = RssFeedGenerator.GetRssFeedData(rssFeedViewModel, Url);
return new RssActionResult { Feed = feed };
}
Second line in the above method is of more importance. Some properties
required to generate the rss feed is passed on to a generator,
RssFeedGenerator
. This class has a method called
GetRssFeedData
to generate the feeds. This method generates a
SyndicationFeed
item, which contains a list of
SyndicationItem
instances. Every item corresponds to a post in the blog ordered by the
most recent item. Note that this feed does not include pages. Finally in
the last line of the method an action result of type
RssActionResult
, we created earlier is returned, by passing in the feed item.
Faqs
Why reinvent the wheel?
Yes, of course, we already have wordpress and a few blog engines in
ASP.Net. But, I love wordpress' approach to blogging and so wanted to
create something that gives the same experience, using ASP.Net MVC 3,
which according to my opinion is one of the best creations of Microsoft!
This has been a very good learning experience and I believe it would be
the same for all others who are going to experiment with this project. A
promise to all of you - this project isn't/won't just be a replica all
along and you can expect some interesting features to be added in the
near future! This project is highly modularized, so that you can easily
plug out something and modify them with your own code. Navigating
through the poject would also be simple, as it does not attempt to
implement a huge number of features or does not use any complicated
logic / code. A number of asp.net based blog engines / cms are created
using webforms and quite a few blog engines/cms use mvc 3. So I wanted
to create a "simple" blogging experience like wordpress, instead of
creating something complex along the lines of orchard (I do agree that
it's awesome, but for a beginner in orchard, like me, it was painful to
setup and debug issues that arose).
Why didn't you use the Entity Framework
At the point I started working on this, I was not very proficient/
confident with the Entity Framework. (I am getting better though!). But I
did provide ways to replace the current database module with anything
else - for instance the entity framework or MySQL or even XML!
Why didn't you use the Disqus commenting system?
Unfortunately, I wasn't aware about Disqus at the time I was designing
and coding the system. So in the next releases I plan to include this
too.
Okay, fair enough. Is the current commenting system thread-based?
Sorry, at this point no. But, this is also in the roadmap for tasks to be done in the next release!
Should we drill in to the code myself or are there going to be any "knowledge base"?
There will soon be a blog, where I periodically plan to put some stuff
about the design and also about how some of the stuff in here could be
changed.
Okay, sBlog.Net has support for multiple users, but only one admin?
Unfortunately, at this point, yes. There could just be one admin user.
But in the next release, you will have the ability to create/use roles.
So that, multiple users can be admins and share the burden with blog
owner!
Do you have any plans for the next release?
Of course! some of the features I have planned to add in the next
release are (apart from the ones mentioned in the earlier questions)...
- A tag listing page based on usage
- An authors page that lists the authors of the blog, with the ability to list the authors posts
- Ability to use the Disquss commenting system instead of the native commenting system
- Threading support for the native commenting system
Points of Interest
I did have a lot of interesting moments while working on creating this
blog engine. One of them is the part where I generate the unique slug. I
had to update the method a few times, since at every step I found issue
that I had not foreseen, for example, I was generating "web.config" as a
valid slug, even though it wasn't!
Another interesting issue I had to tackle was when I was designing the
commenting system, as it's crucial to a blog. Commenting enables the
blog owner to receive readers' opinions. The commenting section above
covered my thoughts regarding this module in detail. Nowadays, at work, I
have been working on a performance critical application as it operates
on very large datasets. I would like to put that in to use for this blog
engine too as I am hell-bent on improving the performance of this blog
engine. Even now, the performance is pretty good, but would definitely
love to make the start-up times even better
with the help of some of the most advanced techniques used in CMS
engines such as Orchard.
History
- Added a new section about the commenting system
- Added a new section about view engines
- Updated the dependency injection and custom model binding sections
- Version 1 of the article released
0 comments:
Post a Comment