pr0g33k

 collapse all
  1. Managing Files Using Microsoft Azure Blob Storage (Part 1)

    Since I'm using Microsoft Azure to host this blog, it just makes sense that I use Azure Blob Storage to store my images for this blog. I had a tough time finding information about configuring and using Azure Blob Storage with an existing MVC Web application, though. So that's what this little walkthrough is about.

    Before we get to the code, you're going to want to download the Azure Storage Emulator. You can download the Azure Storage Emulator (v1.8) here

    To access the Azure Storage Emulator from your Web application, add the following to your Web.config connectionStrings section:

    <connectionStrings>
        <add name="StorageConnection" connectionString="UseDevelopmentStorage=true" />
    </connectionStrings>
        

    Once your project is deployed, you're going to need a different StorageConnection. I use a transformation in my Web.Release.config to handle this.

    <connectionStrings>
        <add name="StorageConnection" connectionString="DefaultEndpointsProtocol=http;AccountName=[YOUR_ACCOUNT_NAME];AccountKey=[YOUR_ACCOUNT_KEY]" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
    </connectionStrings>
        

    Next, install the Windows Azure Storage NuGet package to your MVC Web project.

    That's all you should need to set up your project.

    For my blog, I wanted to store images in one folder and other files in another folder. Azure Blob Storage doesn't really have a concept of "folders," though (not in the same way as the Windows file system, anyway). You can, however, set up "containers" so I'm going to create a container named "images" and a container named "files."

    Now add a controller named "FileManagerController" and a view for Index().

    public class FileManagerController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
        

    We'll fill in the Index.cshtml view in part 2 but for now just create an empty view.

    @{
        ViewBag.Title = "File Manager";
    }
        

    Let's add some code to upload files. To help pass values to my views, I created the following class:

    public class UploadModel
    {
        public String Container { get; set; }
        public String[] Accept { get; set; }
    }
        

    The Container property holds the Azure Blob Storage container name. The Accept string array property contains the file extensions that the user is allowed to upload. I know that I could have used the HTML input tag's "accept" property but I wanted to limit the image types that could be uploaded as well as limit the file types to a specific set. It makes more sense for my application to handle the validation on the server.

    Now let's create a controller action in the FileManagerController to begin the upload process. I use the "id" Route Parameter to pass the container name to the controller. For now, I just want to have 2 containers - images and files - so I enforce that by only accepting those two container names. I also populate the Accept array with the extensions I'll allow to be uploaded.

    public ActionResult Upload(String id)
    {
        switch (id.ToLower())
        {
            case "images":
                return View(new UploadModel() { Container = "images", Accept = new String[] { ".jpg", ".png", ".gif" } });
            case "files":
                return View(new UploadModel() { Container = "files", Accept = new String[] { ".zip", ".txt", ".docx", ".pdf" } });
        }
    
        return RedirectToAction("Index");
    }
        

    The Upload.cshtml looks like this:

    @model RobertGaut.Pr0g33k.Web.Areas.Admin.Models.UploadModel
    @{
        ViewBag.Title = "Upload";
    }
    @using (Html.BeginForm("UploadResults", "FileManager", FormMethod.Post, new { enctype = "multipart/form-data" }))
    { 
        <fieldset>
            <legend>Upload @String.Join(", ", Model.Accept)</legend>
            <ol>
                <li>
                    <input name="input1" type="file">
                </li>
                <li>
                    <input name="input2" type="file">
                </li>
                <li>
                    <input name="input3" type="file">
                </li>
                <li>
                    <input name="input4" type="file">
                </li>
                <li>
                    <input name="input5" type="file">
                </li>
            </ol>
            @Html.HiddenFor(m => m.Container)
            @for (Int32 i = 0; i < Model.Accept.Length; i++)
            {
                @Html.HiddenFor(m => m.Accept[i])
            }
            <input type="submit" value="Upload" />
        </fieldset>
    }
        

    There are a few things to take note of here. First, the form posts using enctype = "multipart/form-data" to ensure that the file data is property encoded in the post. Second, I want to pass the file extensions through to the UploadResults controller action and @Html.HiddenFor(m => m.Accept) will not work the way you might expect because Accept is a String array. To post the contents of the array, you need to loop through the array and output each item in its own hidden form field.

    On the receiving end of the form post we have the UploadResults controller function:

    [HttpPost]
    public ActionResult UploadResults(UploadModel uploadModel)
    {
        CloudBlobContainer cloudBlobContainer = GetCloudBlobContainer(uploadModel.Container);
        HttpPostedFileBase httpPostedFile;
        CloudBlockBlob cloudBlockBlob;
        List<UploadResult> uploadResults = new List<UploadResult>();
        String fileName;
    
        foreach (String file in Request.Files)
        {
            httpPostedFile = Request.Files[file] as HttpPostedFileBase;
            fileName = String.Concat(Path.GetFileNameWithoutExtension(httpPostedFile.FileName).ToSlug().ToLower(), Path.GetExtension(httpPostedFile.FileName));
    
            if (httpPostedFile.ContentLength > 0 && uploadModel.Accept.Contains(Path.GetExtension(fileName)))
            {
                cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(fileName);
                cloudBlockBlob.Properties.ContentType = httpPostedFile.ContentType;
                cloudBlockBlob.UploadFromStream(httpPostedFile.InputStream);
    
                uploadResults.Add(new UploadResult() { FileName = fileName, ContentLength = httpPostedFile.ContentLength, Url = cloudBlockBlob.Uri.ToString() });
            }
            else if (!String.IsNullOrEmpty(fileName))
                uploadResults.Add(new UploadResult() { FileName = fileName, ContentLength = httpPostedFile.ContentLength, Message = "The file is either empty or the file type (extension) is not allowed." });
        }
    
        ViewBag.ResourceType = uploadModel.Container;
    
        return View(uploadResults);
    }
        

    There are a few places where we'll need to get a CloudBlobContainer so I refactored that functionality out to its own private method:

    private CloudBlobContainer GetCloudBlobContainer(String container)
    {
        CloudStorageAccount cloudStorageAccount;
    
        if (Request.IsLocal)
            cloudStorageAccount = CloudStorageAccount.DevelopmentStorageAccount;
        else
            cloudStorageAccount = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnection"].ConnectionString);
    
        CloudBlobClient cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient();
        CloudBlobContainer cloudBlobContainer = cloudBlobClient.GetContainerReference(container.ToLower());
    
        if (cloudBlobContainer.CreateIfNotExists())
        {
            BlobContainerPermissions permissions = cloudBlobContainer.GetPermissions();
            permissions.PublicAccess = BlobContainerPublicAccessType.Container;
            cloudBlobContainer.SetPermissions(permissions);
        }
    
        return cloudBlobContainer;
    }
        

    If the container does not yet exist, it is created when GetCloudBlobContainer is called. I also set the permissions to allow public access when the container is created.

    The Azure Blob Storage is kind of picky about the characters it allows for file names. I wanted to use URL-friendly names - without a lot of URL encoding - so I used my extension method, .ToSlug(), to accomplish that in the UploadResults controller action. I have another post here about that extension method.

    The UploadResults view displays a list of hyperlinks for each file that was successfully uploaded. It also displays a list for any files that were not uploaded because their file size was 0 bytes or because the file type was not allowed.

    @model List<RobertGaut.Pr0g33k.Web.Areas.Admin.Models.UploadResult>
    @using RobertGaut.Core.Extensions
    @{
        ViewBag.Title = "Upload";
    }
    <p>@Html.ActionLink(String.Format("Upload {0}", ((String)ViewBag.ResourceType).ToTitleCase()), "Upload", new { id = ((String)ViewBag.ResourceType).ToTitleCase() })</p>
    @if (Model.Count > 0)
    {
        <text>
        @if (Model.Where(m => String.IsNullOrEmpty(m.Message)).Count() > 0)
        {
            <p>The following files were uploaded:</p>
            <ul>
                @foreach (var uploadResult in Model.Where(m => String.IsNullOrEmpty(m.Message)))
                {
                    <li><a href="@uploadResult.Url" target="_blank">@uploadResult.FileName</a> [@uploadResult.ContentLength bytes]</li>
                }
            </ul>
        }
        <br />
        @if (Model.Where(m => !String.IsNullOrEmpty(m.Message)).Count() > 0)
        {
            <p>The following files were <strong>not</strong> uploaded:</p>
            <ul>
                @foreach (var uploadResult in Model.Where(m => !String.IsNullOrEmpty(m.Message)))
                {
                    <li>@uploadResult.FileName [@uploadResult.ContentLength bytes]</li>
                }
            </ul>
        }
        </text>
    }
        

    To keep my names consistent, I use the following extension method to convert the container name to title case if it was changed:

    public static String ToTitleCase(this String s)
    {
        if (!String.IsNullOrEmpty(s))
        {
            String[] words = s.Split(new Char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            StringBuilder stringBuilder = new StringBuilder();
            Char[] letters;
    
            foreach (String word in words)
            {
                letters = word.ToLower().ToCharArray();
                letters[0] = Char.ToUpper(letters[0]);
                stringBuilder.AppendFormat("{0} ", new String(letters));
            }
    
            return stringBuilder.ToString().Trim();
        }
    
        return s;
    }
        

    In part 2, we'll create a view to display the files we uploaded.

  2. Encode/Decode a URL for use as a Route Parameter

    I've been working on integrating this blog with Microsoft's Azure Blob Storage. I'll have more posts on how to set it up in a few days. In the mean time, I found a nice, little gem I thought I'd share.

    I have a page in the admin section of this blog with a controller and views for file management (List.cshtml, Upload.cshtml, Edit.cshtml, etc.). In the List.cshtml view, I have an unordered list driven by an IEnumerable<IListBlobItem> queried from the Azure CloudBlobContainer. For each URI in IListBlobItem, I create an ActionLink that navigates to Edit.cshtml. I needed to pass the URI as a RouteValue, though, and I was pretty sure that the following wouldn't work very well: http://localhost:1111/FileManager/Edit/http://localhost:10000/images/my-uploaded-image.jpg

    HttpServerUtility.UrlTokenEncode(Byte[] input) & HttpServerUtility.UrlTokenDecode(String input) to the Rescue!

    @Html.ActionLink(Path.GetFileName(url), "Edit", new { id = HttpServerUtility.UrlTokenEncode(System.Text.UTF8Encoding.ASCII.GetBytes(url)) }, new { data_href = url })

    That creates a link that renders like this:

    <a data-href="http://127.0.0.1:10000/devstoreaccount1/images/my-uploaded-image.jpg" href="/Admin/FileManager/Edit/aHR0cDovLzEyNy4wLjAuMToxMDAwMC9kZXZzdG9yZWFjY291bnQxL2ltYWdlcy8xcXZnZnV5bmJrYW1xZ2F6M2pkaTZ3Mi5qcGc1">my-uploaded-image.jpg</a>

    In the FileManager controller, the Edit method accepts the id RouteParameter and decodes the value:

    public ActionResult Edit(String id)
    {
        ViewBag.Url = UTF8Encoding.ASCII.GetString(HttpServerUtility.UrlTokenDecode(id));
        return View();
    }
    

    The "data-href" attribute is used by a jQuery script that opens a preview popup if the file is an image. I'll cover that in a future post.

    Posted on 4/25/2013 at 02:04 PM , Edited on 4/25/2013 at 08:04 PM
    Tags: C#MVC
  3. Farewell, hgroup. We hardly knew thee...

    Alas, the <hgroup> tag is officially gone. Removed from the HTML5 specification. Extinct. Perished. It is now an ex-tag. Last week, this blog validated and the world was happy (well, my little world was happy - I can't really speak for the rest of the world). Then, this week, errors galore. It wasn't a huge change, fortunately (just 3 files and some CSS changes). I wonder what tag is next on the chopping block...

  4. Slugging it out with your URLs

    I've been told that slugs are the very first thing read by search engines. They're that important.

    To some, that statement sounds silly. You might be asking, "What does the common shell-less terrestrial gastropod mollusk have to do with the Internet?" Well, I'm not talking about that kind of slug. I'm talking about those pretty, easy-to-read URL's you see these days. If you clicked through to the specific article of this blog post (click on the title if you haven't already), the URL should be http://www.pr0g33k.com/blog/2/slugging-it-out-with-your-urls. The "slugging-it-out-with-your-urls" part is a slug.

    These days, content is king when it comes to search engine optimization (SEO). The more concise and relevent your content, the better chances you have of getting a higher ranking. Just as the title tag is important for on-site optimization, so are slugs. This is especially true because people will actually read the URL these days to determine if they want to visit a particular site. How many times have you searched for something like "best italian food in dallas, tx" and the first link in the results said "Mama Mia!" but the URL slug said, "20-things-not-to-say-to-your-wifes-face"? You probably wouldn't bother clicking through, right?

    To create slugs, I wrote a very simple extension method to System.String called "ToSlug()." Here's the code:

    public static String ToSlug(this String s)
    {
        return Regex.Replace(Regex.Replace(Regex.Replace(s, @"[^a-zA-Z0-9\s-]", String.Empty), @"[\s-]+", " ").Trim(), @"\s", "-");
    }
    

    To use it, just add a using statement for whatever namespace is necessary (I put mine in a separate assembly - Core.Web - and reference it in a using statement as "Core.Web.Extensions"). To get the links in this blog to format with slugs, I do this:

    <h1>@Html.ActionLink(blog.Title, "Article", "Blog", new { id = blog.Id, slug = blog.Title.ToSlug() }, new { title = String.Format("Permalink to {0}", blog.Title), rel = "Bookmark" })</h1>
    

    I hope this helps!

    Posted on 4/18/2013 at 11:04 PM , Edited on 4/19/2013 at 12:04 AM
    Tags: MVCC#SEO
  5. Building a Tag Cloud using MVC, SQL, HTML5, and jQuery

    One of the things I definitely wanted to do when I rebuilt this blog was implement a tag cloud. To make blogging easy, I just wanted to have a comma-separated list of tags that I attached to each blog post. To get the list of tags and their occurrences, I used the following SQL stored procedure:

    Create Procedure [dbo].[Blog_GetTagsForCloud]
    As
    Begin
    	Set NoCount On
    
    	Select LTRIM(RTRIM(T.Value)) As Tag
    		, COUNT(*) As TagCount
    	From dbo.Blog
    		Cross Apply dbo.fn_ParseDelimitedStrings(dbo.Blog.Tags, ',') As T
    	Where dbo.Blog.IsActive = 1
    	Group By LTRIM(RTRIM(T.Value))
     	Order By NEWID()
    End
        

    The fn_ParseDelimitedStrings table-valued function is used in a Cross Apply so that the Tags column can be evaluated for each record in the Blog table. Grouping on the function's Value column gives us a distinct list of tags and in conjunction with the COUNT(*) function, we get a total count of each tag. The "Order By NEWID()" is there to randomize the order of the tags (as random as it can be, anyway). Here's the fn_ParseDelimitedStrings function:

    Create Function [dbo].[fn_ParseDelimitedStrings](@String nvarchar(MAX), @Delimiter char(1))
    Returns @Values Table 
    (
    	Id int Not Null Identity(1,1) Primary Key
    	, Value nvarchar(MAX) Not Null
    )
    As
    Begin
    	If (Right(@String, 1) != @Delimiter)
    		Set @String = @String + @Delimiter
    
    	Declare @StartPosition smallint = 1
    	Declare @EndPosition smallint = CharIndex(@Delimiter, @String)
    
    	While @EndPosition > 0
    	Begin
    		Insert @Values(Value)
    			Select LTrim(RTrim(SubString(@String, @StartPosition, @EndPosition - @StartPosition)))
    
    		Set @String = Stuff(@String, @EndPosition, 1, '')
    		Set @StartPosition = @EndPosition
    		Set @EndPosition = CharIndex(@Delimiter, @String)
    	End
    
    	Return
    End
        

    Once I have the distinct list of tags and their count, I use a nifty little formula to figure out their proportions and ratios compared to the proportions and ratios of the font sizes I want to use. The formula looks like this:

    Font Size = ((([the count of the tag being computed] - [the lowest occurrence]) * ([the largest font size] - [the smallest font size])) / ([the highest occurrence] - [the lowest occurrence])) + [the smallest font size]

    Simple, right? ;^) Translated to C#, it looks like this:

    public Dictionary<String, Int32> GetTagsForCloud()
    {
        Dictionary<String, Int32> items = new Dictionary<String, Int32>();
    
        using (SqlDataReader reader = GetDataReader("dbo.Blog_GetTagsForCloud"))
        {
            while (reader.Read())
                items.Add(Convert.ToString(reader["Tag"]), Convert.ToInt32(reader["TagCount"]));
    
            reader.Close();
        }
    
        Dictionary<String, Int32> tagCloud = new Dictionary<String, Int32>();
    
        if (items.Count > 0)
        {
            Int32 minValue = items.Min(kvp => kvp.Value);
            Int32 maxValue = items.Max(kvp => kvp.Value);
            Int32 divisor = maxValue - minValue;
            Int32 minFont = 12;
            Int32 maxFont = 36;
    
            if (divisor == 0)
                divisor = 1;
    
            foreach (var item in items)
                tagCloud.Add(item.Key, (((item.Value - minValue) * (maxFont - minFont)) / divisor) + minFont);
        }
    
        return tagCloud;
    }
        

    I have a data abstraction layer and that's where the GetTagsForCloud() function resides. The ORM I use maps column names (or aliases) to objects but since this is a little outside my object structure, I just grab a SqlDataReader and iterate it.

    Note that I had to use the "divisor" variable to counter the possibility of minValue and maxValue being the same. That would result in a divide-by-zero error.

    Next I created a partial view (_TagCloud.cshtml) and put it in the Views/Shared folder since I use it in my _Layout.cshtml:

    @inherits RobertGaut.Pr0g33k.Web.Views.Shared.TagCloudView
    <div id="tag-cloud">
        @foreach (var tag in TagCloud)
        {
            @Html.ActionLink(tag.Key, "Index", new { tag = tag.Key }, new { data_font_size = String.Format("{0}", tag.Value), title = String.Format("View all posts tagged with '{0}'", tag.Key) })
        }
    </div>
        

    And, since I just feel funny about putting C# in my Views, I had the partial inherit from this class:

    public abstract class TagCloudView : WebViewPage
    {
        public Dictionary<String, Int32> TagCloud { get; set; }
    
        protected override void InitializePage()
        {
            base.InitializePage();
            TagCloud = BlogManager.Instance.GetTagsForCloud();
        }
    }
        

    I marked the class "abstract" so that I didn't have to implement the WebPageView's Execute() method. I initially tried to put the call to GetTagsForCloud() in the Execute() method but the timing wasn't right so I moved it to an earlier call, InitializePage(), and the data came through to my partial view just fine.

    Now I had the problem of figuring out how to apply the font size to the hyperlink. I try to avoid using inline styles with HTML5. People keep telling me that's soooo XHTML Transitional 1.0 so I just avoid it altogether. But I definitely didn't want to have to do it with a class attribute and define 24 class selectors! Let me explain. If you look back up at the GetTagsForCloud() method, you'll notice that I define 2 variables:

    Int32 minFont = 12;
    Int32 maxFont = 36;
        

    I'm using them in the formula to make a ratio comparison to the current occurrence and the minimum occurrence. Basically, I'm setting a range from 12 to 36 which I'll translate to "12px" to "36px." If I used a style selector for each size, they'd look something like this:

    .font12 {
        font-size: 12px;
    }
    
    /* the in-between definitions would go here if I wasn't too lazy to type them */
    
    .font36 {
        font-size: 36px;
    }
        

    That's just plain nasty. So instead, I added the value to an HTML5 "data-" attribute. Check out the _TagCloud.cshtml partial view and you'll notice the overload for htmlAttributes has the following:

    data_font_size = String.Format("{0}", tag.Value)
        

    If you tried to type it as "data-font-size" the way it is done in the actual HTML element, you'd get red squigglies telling you that that's no bueno – you can't use hyphens there because it needs to translate the word to a variable name when Razor parses/compiles the statement. By using underscores, though, the Razor view engine outputs the attribute with hyphens. Which, I thought, is pretty darn sweet. Now that I have anchor elements output to the page I can use the following jQuery script to set the font size using the data-font-size attribute.

    <script>
        $(document).ready(function () {
            $('#tag-cloud > a').each(function () {
                $(this).css({ 'font-size': $(this).attr('data-font-size') + 'px' });
            });
        });
    </script>
        

    UPDATE

    Alternatively, you can output the tag cloud using @Html.RenderAction(). I don't know why I didn't do this in the first place. Anyway, here's what the controller looks like:

    public class SharedController : Controller
    {
        public ActionResult TagCloud()
        {
            return PartialView(BlogManager.Instance.GetTagsForCloud());
        }
    }
        

    I put it in a "shared" controller because, well, _Layout.cshtml doesn't really have a controller associated with it. The partial view looks like this:

    @model System.Collections.Generic.Dictionary<String, Int32>
    <div id="tag-cloud">
        @foreach (var tag in Model)
        {
            @Html.ActionLink(tag.Key, "Index", new { tag = tag.Key }, new { data_font_size = String.Format("{0}", tag.Value), title = String.Format("View all posts tagged with '{0}'", tag.Key) })
        }
    </div>
        

    In _Layout.cshtml, I replaced the call to @{ Html.Partial("_TagCloud"); } with @{ Html.RenderAction("TagCloud", "Shared"); }

    This is a much better solution, IMHO.