Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Tags

Wednesday, 15 December, 2010 @ 9:21 PM < Adam Boddington
Tags: ASP.NET MVC, Building Neno, Linq, NHibernate

This is post #22 in the Building Neno series. Please click here for a description of the Building Neno project and instructions on how to access the source code for this post.

In my post new/edit views I want to be able to add (and remove) tags easily as part of the create/update process. Probably the easiest way to do that is have a single text input displaying tags in a comma-delimited string. New tags can be added to the string and existing tags can be removed.

Occasionally I call views "screens" and partial views "controls". Old habits die hard. Hopefully you will know what I mean when I accidentally revert.

The tag string sounds great, but my Post domain class uses an ISet<Tag> collection. How will an ISet translate to and from a comma-delimited string?

View Model

View models to the rescue. I'm currently passing a Post domain object directly to the post new/edit views. I'll change that to a new view model, with a bit of inbuilt tag transformation.

public class Edit : PostView
{
    // ...

    public DomainModel.Post Post { get; private set; }

    // ...

    public string Tags
    {
        get
        {
            string[] tags = Post.Tags.OrderBy(tag => tag.Name).Select(tag => tag.Name).ToArray();

            return string.Join(", ", tags);
        }
        set
        {
            value = value ?? string.Empty;

            IEnumerable<string> tags = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(tag => tag.Trim())
                .Where(tag => !string.IsNullOrWhiteSpace(tag));

            // Remove tags from the post that are not in the string.
            Post.Tags.RemoveAll(Post.Tags
                .Where(t => !tags.Any(tag => tag == t.Name))
                .ToArray());

            // Add tags to the post that are in the string and not in the post.
            Post.Tags.AddAll(tags
                .Where(tag => !Post.Tags.Any(t => t.Name == tag))
                .Select(tag => Container.Get<ITagService>().GetTagByName(tag) ?? new DomainModel.Tag { Name = tag })
                .ToArray());
        }
    }
}

The interesting bit here is the retrieval of tags from a new ITagService and the creation of new tags on the spot if they don't exist. By attaching new tags to my post, I'm expecting NHibernate to create/update the tags when I tell it to create/update the post.

Cascade Save Update

I'll run that and see what happens.

No Cascade

Oops, I haven't told NHibernate it needs to cascade create and update operations on posts to the tag collection as well. That's easily fixed with an explicit cascade attribute in my post NHibernate mapping file.

<set name="Tags" cascade="save-update" mutable="true" table="Neno_Post_Tags">
    <key column="Post" />
    <many-to-many class="StackingCode.Neno.DomainModel.Tag, StackingCode.Neno" column="Tag" />
</set>

And it works. Now, whenever a post is created or updated, the new and modified tags in its tag collection will also be created or updated.

Tag Names

So I've got tags on posts now, but I would also like the tags to be links the user can click through to find other posts with the same tag. I want the URL for that click through to include the tag name. But there's a small problem with that...

  • http://stackingcode.com/blog/tags/ASP.NET%20MVC
  • http://stackingcode.com/blog/tags/Building%20Neno

Ugly, ugly URLs. Capitals, encoded spaces, and eventually other encoded special characters too. Yuck. What I need is sluggified tag names, like this...

That means adding slugs to tags.

Tag Slugs

I'm going to build this out like I did posts. Posts have a title and slug property with the slug automatically generated from the title if a value isn't provided. I'll do the same here with name and slug.

public class Tag : Entity<int>
{
    private string _slug;

    [Required]
    [StringLength(50)]
    public virtual string Name { get; set; }

    [Required]
    [StringLength(50)]
    public virtual string Slug
    {
        get
        {
            string slug = _slug.Sluggify();

            if (string.IsNullOrWhiteSpace(slug))
                return Name.Sluggify();

            return slug;
        }
        set { _slug = value; }
    }
}

I like this approach because it will let me tweak the slugs when I need to (after I build the administration views). If I have a tag named "C#", I can tweak the slug to "c-sharp" instead of the automatic "c".

In case you're wondering, here's what the Sluggify method looks like. It’s been around since the first iteration of my domain model, just doing its thing.

public static class StringExtensions
{
    private const string SLUG_SEPARATOR = "-";

    public static string Sluggify(this string source)
    {
        if (source == null)
            return null;

        // Replace the whitespace and full stops with separators.
        source = Regex.Replace(source, @"[\s\.]+", SLUG_SEPARATOR);

        // Drop the slug to lowercase.
        source = source.ToLower();

        // Get rid of everything that is not a dash, number, underscore or letter.
        source = Regex.Replace(source, @"[^-\d_a-z]+", string.Empty);

        // Shrink the separators.
        source = Regex.Replace(source, SLUG_SEPARATOR + "+", SLUG_SEPARATOR);

        // Trim the surrounding separators.
        source = Regex.Replace(source, string.Format("(^{0}|{0}$)", SLUG_SEPARATOR), string.Empty);

        return source;
    }
}

Changes to the Tag class mean changes to the tag table too.

[Slug]    NVARCHAR (50) NOT NULL,

And changes to the tag NHibernate mapping file as well.

<property name="Slug" />

Booyah, slugs are showing up in the database.

Tag Views and Partial Views

All that's left are a couple of tag views, and a tag partial view for the master page. I already have a method on the new ITagService which will get me all the tags, and I just added one that will get me a tag by its slug. Wouldn't it be nice if there was a collection of posts on each tag that I could just count or iterate through as required? That would save me a lot of time in building out these views.

The many-to-many relationship is already in the database and defined in NHibernate from post to tag. I just need to define the reverse, from tag to post, and I'll have my wish. First, another change to the Tag class.

public class Tag : Entity<int>
{
    // ...

    public Tag()
    {
        Posts = new HashedSet<Post>();
    }

    // ...

    public virtual ISet<Post> Posts { get; private set; }
}

The collection will be lazy by default so this won't result in unnecessary retrievals. Next, a quick addition to the tag NHibernate mapping file.

<set name="Posts" mutable="false" table="Neno_Post_Tags">
    <key column="Tag" />
    <many-to-many class="StackingCode.Neno.DomainModel.Post, StackingCode.Neno" column="Post" />
</set>

The database table already exists, so I’m done.

Posts on tags makes the tag views (and the partial view) a snap to build out. Here's the tag partial view as an example. It displays a link and a post count for every tag.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Tag>>" %>
<h4>Tags</h4>
<p>
<%
    foreach (Tag tag in Model)
    {%>
<%=Html.ActionLink(tag.Name, "postsbytagslug", "tag", new { TagSlug = tag.Slug }, null)%> (<%=tag.Posts.Count%>)<br />
<%
    }%>
</p>

Tags are finished. Or are they?

There are 0 comments.


Comments

Leave a Comment

Please register or login to leave a comment.


Older
Comments

Newer
NHibernate Filters

Older
Comments

Newer
NHibernate Filters

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM