pr0g33k

 collapse all
  1. jQuery Collapsible Panel

    I recently had a need for a JavaScript-driven collapsible panel and noticed that jQuery doesn't have one. I investigated using a jQuery accordion but wasn't satisfied with what it took to get it a single accordion panel to work. So I created my own:

    (function ($) {
        $.fn.extend({
            collapsiblePanel: function () {
                var s = document.createElement('SPAN');
                $(this).each(function () {
                    var i = $(s), h = $(this).find('.ui-widget-header').first(), c = $(this).find('.ui-widget-content').first();
                    if (h && c) {
                        i.text("+").css({ 'display': 'inline-block', 'margin-right': '10px' }).addClass('ui-icon ui-expander ui-icon-triangle-1-e');
                        c.css({ 'padding': '10px' }).hide();
                        h.prepend(i).css({ 'padding': '5px' }).click(function () {
                            c.slideToggle(250, function () {
                                c.is(':visible') ? i.removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-s') : i.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');
                            });
                        });
                    }
                });
            }
        });
    })(jQuery);

    The markup would look like this:

    <div class="ui-widget collapsible-panel">
        <div class="ui-widget-header">Lorum Ipsum</div>
        <div class="ui-widget-content">
            <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean dignissim pharetra ultricies. Nullam a nisl ligula. Integer at lacus a dui volutpat facilisis in at lacus. Mauris nec vestibulum ipsum. In hac habitasse platea dictumst. Vestibulum et justo at urna tempus porttitor et tincidunt dui. Pellentesque hendrerit consequat felis eu sodales. Donec tincidunt laoreet lacus dapibus ullamcorper. Duis ut rutrum orci, ultrices lobortis ipsum.</p>
        </div>
    </div>
    <script>
        $(document).ready(function () {
            $('.collapsible-panel').collapsiblePanel();
        });
    </script>

    Enjoy!

    Posted on 6/5/2015 at 06:06 PM , Edited on 6/5/2015 at 08:06 PM
    Tags: jQuery
  2. Managing Files Using Microsoft Azure Blob Storage (Part 3)

    In part 1, we set up an MVC project to connect to Microsoft's Azure Blob Storage and also created a view to upload some files. In part 2, we created a view to list the files we uploaded. Now let's add a view to display an individual file and delete it.

    First, we need to create an object to pass aroung our data. I created the following class to handle that:

    public class FileEditModel
    {
        public String Container { get; set; }
        public String Url { get; set; }
        public String FileName { get; set; }
    }
        

    Next, add an action to the FileManagerController named Edit and create a strongly-typed (FileEditModel) view for it:

    public ActionResult Edit(String id, String container)
    {
        String url = UTF8Encoding.ASCII.GetString(HttpServerUtility.UrlTokenDecode(id));
        FileEditModel fileEditModel = new FileEditModel()
        {
            Container = container,
            Url = url,
            FileName = Path.GetFileName(url)
        };
    
        return View(fileEditModel);
    }
        

    In the List.cshtml view (part 2), there's an ActionLink that links to the Edit ActionResult/view. The "id" RouteParameter is a URL to the file which is encoded using HttpServerUtility.UrlTokenEncode(). I discuss this in more detail here. In our Edit ActionResult, we decode that "id" RouteParameter using HttpServerUtility.UrlTokenDecode(). The container is passed to the Edit ActionResult as a QueryString parameter. I debated whether I should attempt to parse the container from the URL but decided to explicitly pass it in the QueryString. I realize that it's possible for someone to tamper with the QueryString but this is, after all, intended for the "admin" section of the site and presumably limited to someone in an administrator role who knows what they're doing. If you're not able to trust the person accessing this functionality, you might consider parsing the container from the URL or finding another way to pass the container so that it's more foolproof.

    The Edit.cshtml view looks like this:

    @model RobertGaut.Pr0g33k.Web.Areas.Admin.Models.FileEditModel
    @{
        ViewBag.Title = "Edit";
    }
    <p>@Model.Url</p>
    <p><a href="@Model.Url" target="_blank">Download File/Open in a new window</a></p>
    <div id="preview"></div>
    @using (Html.BeginForm())
    {
        @Html.HiddenFor(m => m.Container)
        @Html.HiddenFor(m => m.FileName)
        @Html.HiddenFor(m => m.Url)
        <p><input name="actionType" type="submit" value="Delete" onclick="return confirm('Are you sure you want to delete this file?')" /></p>
    }
    @section scripts{
        <script>
            $(document).ready(function () {
                var href = "@Model.Url";
                var ext = href.substr(href.lastIndexOf('.') + 1);
                switch (ext) {
                    case "jpg":
                    case "png":
                    case "gif":
                        $("#preview").html("<img id='preview-image' src='" + href + "' /><div></div>");
                        $("#preview-image").load(function () {
                            $("#preview").show();
                            var h = $(this).height();
                            var w = $(this).width();
                            var h2 = h > 240 ? 240 : h;
                            var w2 = Math.floor((h2 / h) * w);
                            $(this).css({ "height": h2 + "px", "width": w2 + "px" });
                            $("#preview").css({ "width": w2 + "px" });
                            $("#preview > div").first().text(w + " X " + h);
                        });
                        break;
                }
            });
        </script>
    }
        

    The jQuery script is similar to the script used on the List.cshtml view - it just displays an icon for the file type and creates a hover effect to display an image preview for image file types.

    I didn't add any functionality to rename or otherwise edit the file, just the ability to delete it. I'm not really interested in renaming and, really, it requires deleting and uploading a new file anyway. There isn't a way in the Azure Blob Storage API to rename blobs, per se. There is a way to copy a blob with a new name but the result is the same - create a new blob with the desired name and delete the old one.

    Posting takes you here:

    [HttpPost]
    public ActionResult Edit(String actionType, FileEditModel fileEditModel)
    {
        CloudBlobContainer cloudBlobContainer = GetCloudBlobContainer(fileEditModel.Container);
        CloudBlockBlob cloudBlockBlob = cloudBlobContainer.GetBlockBlobReference(fileEditModel.FileName);
    
        switch (actionType.ToLower())
        {
            case "delete":
                cloudBlockBlob.DeleteIfExists();
                break;
        }
    
        return RedirectToAction("List", new { id = fileEditModel.Container });
    }
        

    I used a switch here to allow future growth for other features (I know it looks silly to have a switch with just one option). GetCloudBlobContainer is defined in part 1. The code should be pretty self-explanatory: get a reference to the container, use the container to get a reference to the blob, delete the blob is if exists, go back to the list.

    I've been using this for a few days now and the workflow for managing files suits my needs pretty well (for now). I generally like to keep things very simple and straightforward. If you have any suggestions on other features or how to make this better, though, I'd love to hear them.

  3. Managing Files Using Microsoft Azure Blob Storage (Part 2)

    In part 1, we set up an MVC project to connect to Microsoft's Azure Blob Storage and also created a view to upload some files. Now let's add a view to list the files we've uploaded.

    To start, add an ActionResult function to the FileManagerController named "List" and create a view for it (List.cshtml).

    Now, add some ActionLink's to the Index.cshtml view to navigate to our List view.

    @{
        ViewBag.Title = "File Manager";
    }
    <p>@Html.ActionLink("View Images", "List", new { id = "Images" })</p>
    <p>@Html.ActionLink("View Files", "List", new { id = "Files" })</p>
        

    The ActionLinks navigate to our List view - one for images and one for files. The URL's are: "/FileManager/List/Images" and "/FileManager/List/Files"

    To pass the name of the container and the list of URL's from Azure Blob Storage, I created the following class and placed it in my Models folder:

    public class ResourcesModel
    {
        public String Container { get; set; }
        public List<String> Urls { get; set; }
    }
        

    Change the List controller action to look like this:

    public ActionResult List(String id)
    {
        ResourcesModel resourcesModel = new ResourcesModel();
    
        if (!String.IsNullOrEmpty(id))
        {
            CloudBlobContainer cloudBlobContainer = GetCloudBlobContainer(id);
            BlobResultSegment blobResultSegment = cloudBlobContainer.ListBlobsSegmented(null);
    
            resourcesModel.Container = id;
            resourcesModel.Urls = blobResultSegment.Results.Select(r => r.Uri.ToString()).ToList();
    
            while (blobResultSegment.ContinuationToken != null)
            {
                blobResultSegment = cloudBlobContainer.ListBlobsSegmented(blobResultSegment.ContinuationToken);
                resourcesModel.Urls.AddRange(blobResultSegment.Results.Select(r => r.Uri.ToString()));
            }
        }
    
        return View(resourcesModel);
    }
    

    The GetCloudBlobContainer function is listed in part 1.

    I use CloudBlobContainer.ListBlobsSegmented() here because the number of blobs you can return from a single call is limited to 5000. If there are more than 5000 blobs, a continuation token is sent back with the request. The continuation token is basically a marker that can be used by subsequent calls to pick up where the last call left off. First, I call ListBlobsSegmented and pass null for the continuation token to get as many items up to the limit. Then, I loop for as long as a continuation token is returned and get those results, too. I'm only interested in the URI of each blob so I use the Linq Select() extension method to query out the URI's and add them to my Urls (List<String>) collection.

    Here's the List.cshtml view:

    @model RobertGaut.Pr0g33k.Web.Areas.Admin.Models.ResourcesModel
    @using RobertGaut.Core.Extensions
    @{
        ViewBag.Title = "Resources";
    }
    <p>@Html.ActionLink(String.Format("Upload to {0}", Model.Container.ToTitleCase()), "Upload", new { id = Model.Container.ToTitleCase() })</p>
    <ul id="resources">
        @foreach (String url in Model.Urls)
        {
            <li>@Html.ActionLink(Path.GetFileName(url), "Edit", new { id = HttpServerUtility.UrlTokenEncode(System.Text.UTF8Encoding.ASCII.GetBytes(url)), container = Model.Container }, new { data_href = url })</li>
        }
    </ul>
    <div id="preview"></div>
    @section scripts{
        <script>
            $(document).ready(function () {
                $("#resources > li").each(function () {
                    var lip = $(this).position();
                    var liw = $(this).width();
                    var a = $(this).children('a').first();
                    var href = a.attr("data-href");
                    var ext = href.substr(href.lastIndexOf('.') + 1);
                    $(this).css({ "background": "url(/images/" + ext + ".png) no-repeat 50% 0" });
                    switch (ext) {
                        case "jpg":
                        case "png":
                        case "gif":
                            a.hover(function () {
                                $("#preview").html("<img id='preview-image' src='" + href + "' /><div></div>");
                                $("#preview-image").load(function () {
                                    $("#preview").show();
                                    var h = $(this).height();
                                    var w = $(this).width();
                                    var h2 = h > 240 ? 240 : h;
                                    var w2 = Math.floor((h2 / h) * w);
                                    $(this).css({ "height": h2 + "px", "width": w2 + "px" });
                                    $("#preview").css({ "position": "absolute", "top": lip.top - h2 + 105 + "px", "left": Math.floor((lip.left + (liw / 2)) - (w2 / 2)) + "px" });
                                    $("#preview > div").first().text(w + " X " + h);
                                });
                            }, function () {
                                $("#preview").html('');
                                $("#preview").hide();
                            });
                            break;
                    }
                });
            });
        </script>
    }
        

    There's a lot going on there, jQuery-wise, but basically I check the extension of each file and set a background image to the <li> that represents that file type. Then, if the file is an image, I create a hover effect to display a smaller preview. This keeps the browser from downloading every image when this page loads. Images are only downloaded when the user hovers over its anchor tag.

    I have a small amount of CSS that accompanies this view:

    ul#resources {
        margin: 0;
        padding: 0;
    }
    
        ul#resources li {
            display: inline-block;
            height: 142px;
            margin: 5px;
            position: relative;
            min-width: 310px;
        }
    
            ul#resources li a {
                border: 0;
                top: 128px;
                display: block;
                position: relative;
                text-align: center;
                width: 100%;
            }
    
    div#preview {
        background: #000;
        border: 2px solid #000;
        display: none;
        text-align: center;
    }
    
        div#preview > div {
            color: #fff;
            height: 20px;
        }
        

    In part 3 we'll add a view to edit the file.

  4. 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.