The blog of dlaa.me
Tag: "Web Platform"
  • Don't make the audience sit through all your typo-ing [Overview of implementing an extension for WebMatrix - and the complete source code for the Snippets sample]
    Wednesday, October 5th 2011

    When I wrote about WebMatrix's new extensibility features a couple of posts back, I said I'd share the complete source code to Snippets, one of the sample extensions in the gallery. In this post, I'll be doing that along with explaining the overall extension model in a bit more detail. To begin with, here's the code so those of you who want to follow along at home can do so:

    [Click here to download the complete source code for the Snippets extension]

     

    Background

    I created the Snippets extension by starting from the "WebMatrix Extension" Visual Studio Project template I wrote about previously. When expanded, the project template automatically set up the right project references (i.e., Microsoft.WebMatrix.Extensibility.dll) and build steps and created a functioning extension with a single Ribbon button. From there, creating Snippets was a simple matter of adding the functionality I wanted on top of that foundation. But more on that later - I want to go over the default code first.

     

    The default project template extension

    WebMatrix Extension project template

    One key thing to notice is that the template-generated WebMatrixExtension class derives from ExtensionBase, a base class which implements WebMatrix's IExtension interface and simplifies some of the work of dealing with it. In the interest of generality (and because it's just an interface), IExtension doesn't do much beyond identifying a few required properties. ExtensionBase builds on that to offer concrete collections for the IEnumerable(T) properties, automatically MEF Imports the IWebMatrixHost interface, creates an OnWebMatrixHostChanged override, and so on. Of course, none of this is rocket science and the decision to use ExtensionBase is completely up to you. But the whole reason it exists to make your life easier, so I'd suggest at least giving it a chance. :)

    In order for an extension to be loaded by WebMatrix, it needs to be located in the right directory (more on that in the previous post) and it needs to MEF Export the IExtension interface. You might think that ExtensionBase should do the latter for you, but it doesn't because that might restrict your own class hierarchy (i.e., intermediary classes would advertise themselves as extensions even though they're not). Therefore, subclasses like the template-generated WebMatrixExtension class need to explicitly export IExtension.

    With the groundwork out of the way, the basic functionality of the example extension is to add a Ribbon button and handle activation of that button to open the current web site in the browser. Providing content for the Ribbon is as easy as adding instances implementing the relevant interfaces (IRibbonButton, IRibbonMenuButton, IRibbonGroup, etc.) to the generated class's RibbonItemsCollection. It's important to do this exactly once (typically in the extension's constructor) because subsequent changes are not honored (FYI, that may change in the future, but please don't count on it). Of course, you can show and hide Ribbon content whenever you wish; you just can't add or remove items after initialization. Again, there are simple, concrete subclasses for each of the relevant interfaces (IRibbonButton->RibbonButton, etc.) so you don't need to spend time implementing these simple things yourself. And just like before, using the "helper implementations" is completely optional.

    Creating a RibbonButton requires a label, an ICommand implementation, and (optionally) a small/large image. The template's sample includes a couple of images already configured properly to provide a working example. Wiring up the images correctly is standard WPF, but the pack URI syntax can be a little tricky and it's common to forget to change the build type of the image files to "Resource" - so that's already been done for you as a helpful reminder. :) For its ICommand implementation, the template sample uses a simple DelegateCommand class (also included). The sample DelegateCommand is very much in line with other implementations of DelegateCommand or RelayCommand. (Feel free to use whatever version you'd like; the sample DelegateCommand exists simply to avoid introducing a dependency on a third-party library.) As you'd expect, the ICommand's CanExecute and CanExecuteChanged methods are used to dynamically enable/disable the button and its Execute method is called when the button is clicked.

    Yeah, it takes a while to describe what's going on, but there's hardly any code at all! :)

     

    The Snippets extension

    Snippets extension in use

    With the foundation behind us, it's time to consider the Snippets extension itself - and for that, it's helpful to know what it does. Snippets was created with the typical demo scenario in mind: a presenter is showing off WebMatrix and wants to add a block of code to a document, but doesn't want to type it out in front of the audience because that can be slow and boring. Instead, he or she clicks on the Snippets button in the Ribbon, selects from a list of available snippets, and the relevant text is automatically inserted in the editor. Of course, individual snippets should be easy for the user to add or modify and they should allow small and large amounts of text as well as blank lines, etc..

    There are probably a hundred ways you could build this extension; here's how I've done it:

    • Each snippet is stored as a text file (ex: "Empty DIV.txt") in the Snippets folder of the user's Documents folder (i.e., %USERPROFILE%\Documents\Snippets). The file's name identifies the snippet and its contents get added when the snippet is used. You can have as many or few snippet files as you'd like; they're read when the extension is loaded and cached. If there aren't any snippet files (or the Snippets folder doesn't exist), the extension provides a simple message with instructions for how to set things up.

      Aside: WebMatrix needs to be restarted in order for snippet changes to take effect. An obvious improvement would be for the Snippets extension to monitor the Snippets directory for changes and apply them on the fly.
    • The Snippets user interface is a RibbonMenuButton which contains a collection of RibbonButton instances corresponding to each of the available snippets. When the RibbonMenuButton is clicked, it automatically shows the IRibbonButton instances from its Itemscollection in a small, drop-down menu. When a selection is made, the menu is automatically closed.

      Aside: A RibbonSplitButton could have been used if there was a scenario where the user could click the top half of the button to perform a similar action (like inserting a default snippet).
    • The Snippets button only makes sense when the "Files" workspace is active (i.e., a document is being edited), so the extension listens to the IWebMatrixHost.WorkspaceChanged event and shows/hides its Ribbon button according to whether the new workspace is an instance of the IEditorWorkspace interface.

      Aside: One of the bits of feedback we've gotten so far is that this scenario (i.e., "only available for a single workspace") is common and should be simplified. Yep, message received. :)
    • To insert text into the document, Snippets invokes the Paste command via the IWebMatrixHost.HostCommands property. While it's possible to get and set editor text directly for more advanced scenarios, the Paste command works nicely because the editor automatically updates the caret position, replaces selected text, etc.. The downside to using the (application-wide) Paste command is that if the input focus isn't in the body of an open file, then the paste action will be directed elsewhere and won't work as it's meant to.

      Aside: The editor interfaces are almost rich enough to manage everything here and avoid using Paste. However, there were a couple of glitches when I tried which led me to use the simpler paste approach for the sample.
    • Input focus aside, one thing that's clear is that Snippets can never be added without an open document visible. To determine if that's the case, Snippets uses an unnamed host command to populate an instance of IEditorContainer. If that fails or the IEditorExt within is null, that means no document is open and Snippets knows to disable its Ribbon buttons.

      Unnamed host commands work just like named commands (i.e., Paste), though they're a little harder to get to. The HostCommands.GetCommand method exists for this purpose and allows the caller to pass a GUID/ID for the command to return. The relevant code looks like this:

      /// <summary>
      /// Gets an IEditorExt instance if the editor is in use.
      /// </summary>
      /// <returns>IEditorExt reference.</returns>
      private IEditorExt GetEditorExt()
      {
          var editorContainer = new EditorContainer();
          _editorContainerCommand = WebMatrixHost.HostCommands.GetCommand(new Guid("27a0f541-c86c-4f0b-b436-0b50bf9f7ef8"), 10);
          if (_editorContainerCommand != null)
          {
              _editorContainerCommand.Execute(editorContainer);
          }
      
          return editorContainer.Editor;
      }

      Helper properties for this and other unnamed commands be added in the future. For now, FYI about a useful one. :)

    • Although the RibbonButton class supports a parameter parameter for passing to the ICommand's CanExecute/Execute methods to provide additional context, this value is not actually passed through in some cases. :( This bug was found too late to fix for the Beta, but the good news is that it's easy to work around by creating a closure to capture the relevant parameter information instead. If you're not familiar with this technique, it involves defining an anonymous method that references the desired data; the compiler automatically captures the necessary values and passes them along when the delegate gets called.

      Here's what it looks like in the sample (using LINQ to create the collection):

      // Create buttons for each snippet
      var snippetsButtons = _snippets
          .Select(s =>
              new RibbonButton(
                  s.Key,
                  new DelegateCommand(
                      (parameter) => GetEditorExt() != null,
                      (parameter) =>
                      {
                          // parameter is (incorrectly) null, so add this indirection for now
                          HandleSnippetInvoke(s.Value);
                      }),
                  s.Value,
                  _snippetsImageSmall,
                  _snippetsImageLarge));
    • It turns out there's a subtle bug because of the following code in the sample:

      // Paste the snippet into the editor
      var paste = WebMatrixHost.HostCommands.Paste;
      if (paste.CanExecute(insertText))
      {
          paste.Execute(insertText);
      }

      Note that Snippets is invoking a special flavor of the Paste command by providing the text as the parameter property for the CanExecute/Execute methods (meaning "paste this text, please"). However, the underlying editor code is returning a CanExecute result based on whether or not it could paste from the clipboard (i.e., it's not honoring the meaning of the text parameter). Therefore, if the clipboard is empty or contains non-text data like a file, CanExecute returns false and Snippets isn't able to insert text.

      The easy workaround is to copy some text to the clipboard so the underlying implementation will return true for CanExecute and the specialized paste operation will be invoked.

     

    Summary

    Snippets is a small extension that builds on the default WebMatrix Extension project template to implement some useful (if limited) functionality. Its original purpose was to simplify demos, but if people find practical uses for it, that's great, too! :)

    But whether or not people use it, the Snippets extension touches on enough interesting areas of extensibility that people can probably learn from it. If you're getting started with WebMatrix extensions and are looking for a "real world" sample, I hope Snippets can be helpful. If you have feedback or questions - about Snippets or more generally - please let me know!

    Tags: Technical Web Platform
  • Make it your own [WebMatrix's new extensibility support enables developers and applications to customize the web development experience to their liking!]
    Wednesday, September 14th 2011

    In conjunction with Microsoft's ongoing BUILD Windows conference, we have just released betas of the next version of the Microsoft Web Platform. Today's releases include a lot of new functionality and I encourage interested parties to have a look at what's new in ASP.NET and WebMatrix.

    In this post, I'm going to focus on one aspect of the WebMatrix 2 Beta: extensibility. By exposing a public API and opening up the application to others, WebMatrix enables individuals and community members to customize the experience to best fit their own unique processes and workflow. Many aspects of the development experience are configurable, so users can become more productive by streamlining common tasks, automating monotonous ones, and simplifying difficult ones!

     

    Task-based extensibility

    Extensibility for the WebMatrix 2 Beta takes three forms: task-based extensibility, help extensibility, and extensions.

    1. Task-based extensibility refers to the ability of a web application like Umbraco or WordPress to embed a file within the install package to customize the WebMatrix user interface for that particular application. By providing some simple XML, applications in the Web App Gallery can add custom links to the Ribbon or dashboard, protect core application files, provide enhanced Intellisense for PHP, and more.

    2. Help extensibility makes it possible to integrate custom content with WebMatrix's new, context-sensitive help pane. The new help system shows links to relevant content and videos based on where the user is in the application and what he or she is doing. Help content is drawn from a variety of sources; content providers can create custom feeds to cover new topics or provide more context on existing ones. This article explains how to create custom help content.

    3. For developers, the real power lies in the ability to write extensions that run inside WebMatrix because they're capable of far richer customization. WebMatrix extensions can be written in any .NET language, are loaded by MEF, the Managed Extensibility Framework, and installed/uninstalled (behind the scenes) as NuGet packages (with a slight twist I'll explain in a different post). Similar to Visual Studio, WebMatrix has an extension gallery that allows users to browse and install extensions from a central feed - or create and share custom feeds!

     

    ColorThemeManager

    Opening WebMatrix's extension gallery (by clicking the "Extensions/Gallery" Ribbon button from the Site workspace) shows some of the extensions that have been created to help give an idea what's possible. I'll call out three of them:

    • ColorThemeManager - This extension by Yishai Galatzer allows you to customize the colors used by the file editor in WebMatrix, export your settings, and import settings from other sources. So if you're one of those people who enjoys looking at green on black text, then you're in luck. :)

    • ImageOptimizer - This extension by Mads Kristensen makes it easy to "optimize" the PNG and JPEG images of a web site by removing unnecessary metadata and recompressing content to minimize file sizes - thereby keeping bandwidth down and site responsiveness up.

    • Snippets - This extension by me makes it easy to insert snippets of text into an open document in the Files workspace. It was written to make WebMatrix demos a little easier (watch for it in the WebMatrix: uber geek in designer clothes presentation today at BUILD!), but its simplicity makes it a good learning tool, too. I'll be blogging the complete source code for Snippets in a few days.

     

    WebMatrix Extension project template

    In addition to providing an overview of the Snippets sample, I plan to discuss other aspects of WebMatrix extensibility over the next few weeks. In the meantime, you can start exploring WebMatrix extensibility today:

    1. Download the Microsoft.WebMatrix.Extensibility CHM file, unblock it (important: click here for directions on "unblocking" a file), open it, and browse the contents. (If you see the message "Navigation to the webpage was canceled", then the file is still blocked.)

    2. Download the "WebMatrix Extension" Visual Studio project template, save the ZIP file in your "%USERPROFILE%\Documents\Visual Studio 2010\Templates\ProjectTemplates" directory, choose File, New, Project in Visual Studio 2010, select the "Visual C#" node, click the "WebMatrix Extension" item, type a project name (ex: "MyExtension" (no spaces, please)), and click OK.

    The project template sets up the right infrastructure, all the necessary references, includes pre- and post-build rules to make development a little easier, and helps get you started with a simple Ribbon-based extension that demonstrates some of the basic extensibility points. The template's ReadMe.txt explains how to configure Visual Studio so that pressing F5 will automatically load the extension inside WebMatrix for a simple, seamless debugging experience with complete breakpoint support, etc.. FYI that I'd like to improve this template by adding support for NuGet package generation (so it will be easier to deploy extensions to a gallery) and maybe also create a VISX wrapper for it (to enable more seamless install of the template itself).

    Aside: While the project template "helpfully" copies your extension to the right place for WebMatrix to load it on startup, the extension is not properly installed and so WebMatrix doesn't know how to uninstall it. For now, the easiest way to get rid of a custom extension is to close WebMatrix and delete the contents of the "%USERPROFILE%\AppData\Local\Microsoft\WebMatrix\Components" directory.

     

    The new extensibility APIs in the WebMatrix 2 Beta allow developers to get started with extensions today. And while there aren't yet extension points for everything, there are enough to enable some pretty interesting scenarios. Available extension points include:

    Microsoft.WebMatrix.Extensibility help file
    • Ribbon content
      • Buttons, groups, tabs, etc.
    • Application information
      • Name, local path, remote URI
    • Integrated dialog and notification UI
    • Context menu items for application files/directories
    • Editor manipulation
      • Text buffer, settings, theming, custom types
    • Dashboard content
    • Simple commanding
    • Active workspace
    • More...

    That said, people are going to have a lot of great extension ideas that are either difficult or impossible to achieve with the Beta APIs. Not being able to put good ideas into practice is certainly disappointing, but it's also a great opportunity to let us know what features are missing from the API and how we can improve it! To make that easy, there's a WebMatrix forum where you can ask questions and exchange ideas. Once you've tried things out, please go there and share your thoughts!

     

    Aside: It probably goes without saying (but I'll say it anyway!) that the APIs available in the WebMatrix 2 Beta are subject to change and it's likely that extensions written for Beta will need to be modified in order to run on later releases. That's not to discourage people from writing extensions, but rather an attempt to set expectations appropriately. :)
    Further aside: It's natural to wonder if existing plugins for Visual Studio will "just work" in WebMatrix. The answer is that they will not - but in most cases it turns out that trying to load a Visual Studio extension inside WebMatrix wouldn't be all that meaningful anyway... At this point, the functionality of these two products and the target audience (both users and developers) are different enough that things don't align in a way that makes this scenario work.
    Tags: Web Platform
  • When you live on the bleeding edge, be prepared for a few nicks [Minor update to the Delay.Web.Helpers ASP.NET assembly download to avoid a NuGet packaging bug affecting Razor installs]
    Monday, May 9th 2011

    I had a surprise last week when coworker Bilal Aslam mentioned he was using my Delay.Web.Helpers assembly but wasn't able to install the 1.1.0 version with the ASP.NET Razor "_Admin" control panel. (Fortunately, the previous version (1.0.0) did install and had the functionality he needed, so he was using that one for the time being.) I quickly told Bilal he was crazy because I remembered testing with the NuGet plugin for Visual Studio and knew it installed successfully. At which point he demonstrated the problem for me - and I was forced to admit defeat. :)

    Aside: Delay.Web.Helpers is a collection of ASP.NET web helpers that provide access to Amazon Simple Storage Service (S3) buckets and blobs as well as easy ways to create "data URIs". (And eventually more stuff as I get time to add it...)

    Naturally, the first thing I did was to repeat my previous testing in Visual Studio - and it worked fine just like I remembered. So I tried with the Razor administration interface and it failed just like Bilal showed me: "System.InvalidOperationException: The 'schemaVersion' attribute is not declared.". Because the previous version (1.0.0) didn't have this problem, I was a little confused; I'd built everything from the same .nuspec file, so it wasn't clear why the Razor/1.1.0 scenario would be uniquely broken.

    At that point, I contacted a couple folks on the NuGet team and got a quick answer: for some (short) period of time, the official version of nuget.exe created packages with a schemaVersion attribute on the package/metadata element of the embedded .nuspec file and the presence of this attribute causes the Razor install implementation to fail with the exception we were seeing. I'd created 1.0.0 with a good version of nuget.exe, but apparantly created 1.1.0 with the broken version. :(

    The team's recommendedation was to re-create my packages with the current nuget.exe and re-deploy them to the NuGet servers. I did that and the result is version 1.1.1 of the Delay.Web.Helpers package and its associated Delay.Web.Helpers.SampleWebSite package. "Once bitten, twice shy", so I verified the install in both Visual Studio and Razor now that I know they're different and can fail independently.

    Aside: There are no changes to the Delay.Web.Helpers assembly or samples in this release. The only changes are the necessary tweaks to the NuGet metadata for both packages to install successfully under Razor. Therefore, if you've already installed 1.1.0 successfully, there's no need to upgrade.
    Further aside: The standalone ZIP file with the assembly, source code, automated tests, and sample web site is unaffected by this update.

     

    To sum things up, if you created a NuGet package sometime around early April and you expect it to be installable with the Razor administration panel, I'd highly recommend trying it out to be sure! :)

    Tags: Web Platform
  • Images in a web page: meh... Images *in* a web page: cool! [Delay.Web.Helpers assembly now includes an ASP.NET web helper for data URIs (in addition to Amazon S3 blob/bucket support)]
    Wednesday, April 6th 2011
    Delay.Web.Helpers DataUri sample page

    The topic of "data URIs" came up on a discussion list I follow last week in the context of "I'm in the process of creating a page and have the bytes of an image from my database. Can I deliver them directly or must I go through a separate URL with WebImage?" And the response was that using a data URI would allow that page to deliver the image content inline. But while data URIs are fairly simple, there didn't seem to be a convenient way to use them from an ASP.NET MVC/Razor web page.

    Which was kind of fortuitous for me because I've been interested in learning more about data URIs for a while and it seemed that creating a web helper for this purpose would be fun. Better yet, I'd already released the Delay.Web.Helpers assembly (with support for Amazon S3 blob/bucket access), so I had the perfect place to put the new DataUri class once I wrote it! :)

    Aside: For those who aren't familiar, the WebImage class provides a variety of handy methods for dealing with images on the server - including a Write method for sending them to the user's browser. However, the Write method needs to be called from a dedicated page that serves up just the relevant image, so it isn't a solution for the original scenario.

     

    In case you've not heard of them before, data URIs are a kind of URL scheme documented by RFC 2397. They're quite simple, really - here's the relevant part of the specification:

    data:[<mediatype>][;base64],<data>
    
    dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
    mediatype  := [ type "/" subtype ] *( ";" parameter )
    data       := *urlchar
    parameter  := attribute "=" value
    

    It takes two pieces of information to create a data URI: the data and its media type (ex: "image/png"). (Although the media type appears optional above, it defaults to "text/plain" when absent - which is unsuitable for most common data URI scenarios.) Pretty much the only interesting thing you can do with data URIs on the server is write them, so the DataUri web helper exposes a single Write method with five flavors. The media type is always passed as a string (feel free to use the MediaTypeNames class to help here), but the data can be provided as a file name string, byte[], IEnumerable<byte>, or Stream. That's four methods; the fifth one takes just the file name string and infers the media type from the file's extension (ex: ".png" -> "image/png").

    Aside: Technically, it would be possible for the other methods to infer media type as well by examining the bytes of data. However, doing so would require quite a bit more work and would always be subject to error. On the other hand, inferring media type from the file's extension is computationally trivial and much more likely to be correct in practice.

     

    For an example of the DataUri helper in action, here's the Razor code to implement the "image from the database" scenario that started it all:

    @{
        IEnumerable<dynamic> databaseImages;
        using (var database = Database.Open("Delay.Web.Helpers.Sample.Database"))
        {
            databaseImages = database.Query("SELECT * FROM Images");
        }
    
        // ...
    
        foreach(var image in databaseImages)
        {
            <p>
                <img src="@DataUri.Write(image.Content, image.MediaType)" alt="@image.Name"
                     width="@image.Width" height="@image.Height" style="vertical-align:middle"/>
                @image.Name
            </p>
        }
    }

    Easy-peasy lemon-squeezy!

     

    And here's an example of using the file name-only override for media type inference:

    <script src="@DataUri.Write(Server.MapPath("Sample-Script.js"))" type="text/javascript"></script>

    Which comes out like this in the HTML that's sent to the browser:

    <script src="data:text/javascript;base64,77u/ZG9j...PicpOw==" type="text/javascript"></script>
    Aside: This particular example (using a data URI for a script file) doesn't render in all browsers. Specifically, Internet Explorer 8 (and earlier) blocks script delivered like this because of security concerns. Fortunately, Internet Explorer 9 has addressed those concerns and renders as expected. :)

     

    [Click here to download the Delay.Web.Helpers assembly, complete source code, automated tests, and the sample web site.]

    [Click here to go to the NuGet page for Delay.Web.Helpers which includes the DLL and its associated documentation/IntelliSense XML file.]

    [Click here to go to the NuGet page for the Delay.Web.Helpers.SampleWebSite which includes the sample site that demonstrates everything.]

     

    Data URIs are pretty neat things - though it's important to be aware they have their drawbacks as well. Fortunately, the Wikipedia article does a good job discussing the pros and cons, so I highly recommend looking it over before converting all your content. :) Creating a data URI manually isn't rocket science, but it is the kind of thing ASP.NET web helpers are perfectly suited for. If you're a base-64 nut, maybe you'll continue doing this by hand - but for everyone else, I hope the new DataUri class in the Delay.Web.Helpers assembly proves useful!

    Tags: Technical Web Platform
  • Candy-eating lemming [Delay.Web.Helpers (and its easy-to-use Amazon S3 blob/bucket API) is now available via NuGet!]
    Thursday, February 24th 2011

    NuGet is all the rage right now, and it seemed like a good time to familiarize myself with it. Conveniently, it just so happened that I had something perfectly suited: my Delay.Web.Helpers assembly which offers ASP.NET web helper-style access to Amazon Simple Storage Service (S3) blobs and buckets. (For context, here's the introductory post for Delay.Web.Helpers and here's a follow-up discussing the addition of container support.)

    I'm pleased to say I've just published the following two packages to the public NuGet repository:

     

    Delay.Web.Helpers.SampleWebSite folder in Solution Explorer All you need to install to get going is the Delay.Web.Helpers package - that'll get you the binaries and full IntelliSense, too! If you also want some samples to check out, please install the Delay.Web.Helpers.SampleWebSite package.

    Because I don't like packages that add random files to my projects, I've configured the Delay.Web.Helpers.SampleWebSite package to install all its sample code under a same-named folder where it won't get mixed up with anyone's existing content. [You're welcome. :) ] I've tweaked the sample to run cleanly in this configuration, so all you need to do is right-click the Delay.Web.Helpers.SampleWebSite folder in Visual Studio's Solution Explorer and choose View in Browser to see it in action.

    The sample application lets you browse a previously-configured Amazon S3 account, add, view, and remove buckets, and add, view, and remove files within those buckets. It's not the most fully-featured S3 browser ever, but it does a pretty good job showing off the complete AmazonS3Storage API.

    Update at 1:50pm: The sample uses CSHTML (Razor) web pages, so you'll want to add it to a project that supports that file type. From Visual Studio, choose File, New, Web Site..., "ASP.NET Web Site (Razor)". If the resulting web site doesn't run by default due to a missing SQL dependency (i.e., before you add the samples), just comment-out the call to WebSecurity.InitializeDatabaseConnection in _AppStart.cshtml. And if the Razor web site type isn't available at all, please install ASP.NET MVC3 from here.

     

    Notes:

    • The Delay.Web.Helpers assembly included in the NuGet package is exactly the same DLL I previously released on my blog. If you're already using it successfully, there's no need to switch over to the NuGet version.

    • As I add more features in the future, I'll continue releasing bits via my blog and will also update these NuGet packages. So please pick the delivery mechanism you like best (ZIP or NuGet) and feel free to ignore the other one. :)

    • Creating your own NuGet package is quite simple and straightforward. The resources I used were the NuGet CodePlex site and the NuGet Gallery. The former has a link to download the NuGet.exe tool you'll need as well as some good documentation on the process of creating a package. The latter is where you go to upload your package - at which point it's automatically visible to the NuGet plug-in for Visual Studio, the ASP.NET MVC3 application admin interface, etc..

     

    Creating useful libraries and sharing them with others is a great way to contribute back to the community - but only if you can get the bits into the hands of people who want them! NuGet offers a lightweight packaging mechanism that's so simple, anyone can get started with a minimum investment of time or energy. The NuGet gallery has broad developer reach, is easily searchable, and is managed by someone else - so all you need to worry about is writing a good library!

    Less overhead means more development time - thanks, NuGet! :)

    Tags: Technical Web Platform
  • Night of the Living WM_DEADCHAR [How to: Avoid problems with international keyboard layouts when hosting a WinForms TextBox in a WPF application]
    Monday, February 21st 2011

    There was a minor refresh for the Microsoft Web Platform last week; you can find a list of fixes and instructions for upgrading in this forum post. The most interesting change for the WPF community is probably the fix for a WebMatrix problem brought to my attention by @radicalbyte and @jabroersen where Ctrl+C, Ctrl+X, and Ctrl+V would suddenly stop working when an international keyboard layout was in use. Specifically, once one of the prefix keys (ex: apostrophe, tilde, etc.) was used to type an international character in the editor, WebMatrix's copy/cut/paste shortcuts would stop working until the application was restarted. As it happens, the Ribbon buttons for copy/cut/paste continued to work correctly - but the loss of keyboard shortcuts was pretty annoying. :(

    Fortunately, the problem was fixed! This is the story of how...

     

    To begin with, it's helpful to reproduce the problem on your own machine. To do that, you'll need to have one of the international keyboard layouts for Windows installed. If you spend a lot of time outside the United States, you probably already do, but if you're an uncultured American like me, you'll need to add one manually. Fortunately, there's a Microsoft Support article that covers this very topic: How to use the United States-International keyboard layout in Windows 7, in Windows Vista, and in Windows XP! Not only does it outline the steps to enable an international keyboard layout, it also explains how to use it to type some of the international characters it is meant to support.

    Aside: Adding a new keyboard layout is simple and unobtrusive. The directions in the support article explain what to do, though I'd suggest skipping the part about changing the "Default input language": if you stop at adding the new layout, then your default experience will remain the same and you'll be able to selectively opt-in to the new layout on a per-application basis.

    With the appropriate keyboard layout installed, the other thing you need to reproduce the problem scenario is a simple, standalone sample application, so...

    [Click here to download the InternationalKeyboardBugWithWpfWinForms sample application.]

     

    Compiling and running the sample produces a .NET 4 WPF application with three boxes for text. The first is a standard WPF TextBox and it works just like you'd expect. The second is a standard Windows Forms TextBox (wrapped in a WindowsFormsHost control) and it demonstrates the problem at hand. The third is a trivial subclass of that standard Windows Forms TextBox that includes a simple tweak to avoid the problem and behave as you'd expect. Here's how it looks with an international keyboard layout selected:

    InternationalKeyboardBugWithWpfWinForms sample

    Although the sample application doesn't reproduce the original problem exactly, it demonstrates a related issue caused by the same underlying behavior. To see the problem in the sample application, do the following:

    1. Make the InternationalKeyboardBugWithWpfWinForms window active.
    2. Switch to the United States-International keyboard layout (or the corresponding international keyboard layout for your country).
    3. Press Ctrl+O and observe the simple message box that confirms the system-provided ApplicationCommands.Open command was received.
    4. Type one of the accented characters into the WinForms TextBox (the middle one).
    5. Press Ctrl+O again and notice that the message box is no longer displayed (and the Tab key has stopped working).

     

    As you can see, the InternationalTextBox subclass avoids the problem its WinForms TextBox base class exposes. But how? Well, rather simply:

    /// <summary>
    /// Trivial subclass of the Windows Forms TextBox to work around problems
    /// that occur when hosted (by WindowsFormsHost) in a WPF application.
    /// </summary>
    public class InternationalTextBox : System.Windows.Forms.TextBox
    {
        /// <summary>
        /// WM_DEADCHAR message value.
        /// </summary>
        private const int WM_DEADCHAR = 0x103;
    
        /// <summary>
        /// Preprocesses keyboard or input messages within the message loop
        /// before they are dispatched.
        /// </summary>
        /// <param name="msg">Message to pre-process.</param>
        /// <returns>True if the message was processed.</returns>
        public override bool PreProcessMessage(ref Message msg)
        {
            if (WM_DEADCHAR == msg.Msg)
            {
                // Mark message as handled; do not pass it on
                return true;
            }
    
            // Call base class implementation
            return base.PreProcessMessage(ref msg);
        }
    }

    The underlying problem occurs when the WM_DEADCHAR message is received and processed by both WPF and WinForms, so the InternationalTextBox subclass shown above prevents that by intercepting the message in PreProcessMessage, marking it handled (so nothing else will process it), and skipping further processing. Because the underlying input-handling logic that actually maps WM_DEADCHAR+X to an accented character still runs, input of accented characters isn't affected - but because WPF doesn't get stuck in its WM_DEADCHAR mode, keyboard accelerators like Ctrl+O continue to be processed correctly!

    Aside: The change above is not exactly what went into WebMatrix... However, this change appears to work just as well and is a bit simpler, so I've chosen to show it here. The actual change is similar, but instead of returning true, maps the WM_DEADCHAR message to a WM_CHAR message (ex: msg.MSG = WM_CHAR;) and allows the normal base class processing to handle it. I'm not sure the difference matters in most scenarios, but it's what appeared to be necessary at the time, it's what the QA team signed off on, and it's what the WPF team reviewed. :) I'm optimistic the simplified version I show here will work well everywhere, but if you find somewhere it doesn't, please try the WM_CHAR translation instead. (And let me know how it works out for you!)

     

    Hosting Windows Forms controls in a WPF application isn't the most common thing to do, but sometimes it's necessary (or convenient!), so it's good to ensure everything works well together. If your application exposes any TextBox-like WinForms classes, you might want to double-check that things work well with an international keyboard layout. And if they don't, a simple change like the one I show here may be all it takes to resolve the problem! :)

    PS - I mention above that the WPF team had a look at this workaround for us. They did more than that - they fixed the problem for the next release of WPF/WinForms so the workaround will no longer be necessary! Then QA checked to make sure an application with the workaround wouldn't suddenly break when run on a version of .NET/WPF/WinForms with the actual fix - and it doesn't! So apply the workaround today if you need it - or wait for the next release of the framework to get it for free. Either way, you're covered. :)
    Tags: WPF Web Platform
  • "Might as well face it, you're addicted to blob..." [BlobStoreApi update adds container management, fragmented response handling, other improvements, and enhanced Amazon S3 support by Delay.Web.Helpers!]
    Thursday, January 20th 2011

    As part of my previous post announcing the Delay.Web.Helpers assembly for ASP.NET and introducing the AmazonS3Storage class to enable easy Amazon Simple Storage Service (S3) blob access from MVC/Razor/WebMatrix web pages, I made some changes to the BlobStoreApi.cs file it built on top of. BlobStoreApi.cs was originally released as part of my BlobStore sample for Silverlight and BlobStoreOnWindowsPhone sample for Windows Phone 7; I'd made a note to update those two samples with the latest version of the code, but intended to blog about a few other things first...

     

    Fragmented response handling

    BlobStoreOnWindowsPhone sample

    That's when I was contacted by Steve Marx of the Azure team to see if I could help out Erik Petersen of the Windows Phone team who was suddenly seeing a problem when using BlobStoreApi: calls to GetBlobInfos were returning only some of the blobs instead of the complete list. Fortunately, Steve knew what was wrong without even looking - he'd blogged about the underlying cause over a year ago! Although Windows Azure can return up to 5,000 blobs in response to a query, it may decide (at any time) to return fewer (possibly as few as 0!) and instead include a token for getting the rest of the results from a subsequent call. This behavior is deterministic according to where the data is stored in the Azure cloud, but from the point of view of a developer it's probably safest to assume it's random. :)

    So something had changed for Erik's container and now he was getting partial results because my BlobStoreApi code didn't know what to do with the "fragmented response" it had started getting. Although I'd like to blame the documentation for not making it clear that even very small responses could be broken up, I really should have supported fragmented responses anyway so users would be able to work with more than 5,000 blobs. Mea culpa...

    I definitely wanted to add BlobStoreApi support for fragmented Azure responses, but what about S3? Did my code have the same problem there? According to the documentation: yes! Although I don't see mention of the same "random" fragmentation behavior Azure has, S3 enumerations are limited to 1,000 blobs at a time, so that code needs the same update in order to support customer scenarios with lots of blobs. Fortunately, the mechanism both services use to implement fragmentation is quite similar and I was able to add most of the new code to the common RestBlobStoreClient base class and then refine it with service-specific tweaks in the sub-classes AzureBlobStoreClient and S3BlobStoreClient.

    Fortunately, the extra work to support fragmented responses is all handled internally and is therefore invisible to the developer. As such, it requires no changes to existing applications beyond updating to the new BlobStoreApi.cs file and recompiling!

     

    Container support

    While dealing with fragmented responses was fun and a definite improvement, it's not the kind of thing most people appreciate. People don't usually get excited over fixes, but they will get weak-kneed for new features - so I decided to add container/bucket management, too! Specifically, it's now possible to use BlobStoreApi to list, create, and delete Azure containers and S3 buckets using the following asynchronous methods:

    public abstract void GetContainers(Action<IEnumerable<string>> getContainersCompleted, Action<Exception> getContainersFailed);
    public abstract void CreateContainer(string containerName, Action createContainerCompleted, Action<Exception> createContainerFailed);
    public abstract void DeleteContainer(string containerName, Action deleteContainerCompleted, Action<Exception> deleteContainerFailed);

    To be clear, each instance of AzureBlobStoreClient and S3BlobStoreClient is still associated with a specific container (the one that was passed to its constructor) and its blob manipulation methods act only on that container - but now it's able to manipulate other containers, too.

    Aside: In an alternate universe, I might have made the new container methods static because they don't have a notion of a "current" container the same way the blob methods do. However, what I want more than that is to be able to leverage the existing infrastructure I already have in place for creating requests, authorizing them, handling responses, etc. - all of which take advantage of instance methods on RestBlobStoreClient that AzureBlobStoreClient and S3BlobStoreClient override as necessary. In this world, it's not possible to have static methods that are also virtual, so I sacrificed the desire for the former in order to achieve the efficiencies of the latter. Maybe one day I'll refactor things so it's possible to have the best of both worlds - but for now I recommend specifying a null container name when creating instances of AzureBlobStoreClient and S3BlobStoreClient when you want to ensure they're used only for container-specific operations (and not for blobs, too).

     

    Other improvements

    Delay.Web.Helpers AmazonS3Storage sample

    With containers getting a fair amount of love this release, it seemed appropriate to add a containerName parameter to the AzureBlobStoreClient constructor so it would look the same as S3BlobStoreClient's constructor. This minor breaking change means it's now necessary to specify a container name when creating instances of AzureBlobStoreClient. If you want to preserve the behavior of existing code (and don't want to have to remember that "$root" is the magic root container name for Azure (and previous default)), you can pass AzureBlobStoreClient.RootContainerName for the container name.

    In the process of doing some testing of the BlobStoreApi code, I realized I hadn't previously exposed a way for an application to find out about asynchronous method failures. While it's probably true that part of the reason someone uses Azure or S3 is so they don't need to worry about failures, things can always go wrong and sometimes you really need to know when they do. So in addition to each asynchronous method taking a parameter for the Action to run upon successful completion, they now also take an Action<Exception> parameter which they'll call (instead) when a failure occurs. This new parameter is necessary, so please provide a meaningful handler instead of passing null and assuming things will always succeed. :)

    I've also added a set of new #defines to allow BlobStoreApi users to easily remove unwanted functionality. Because they're either self-explanatory or simple, I'm not going to go into more detail here (please have a look at the code if you're interested): BLOBSTOREAPI_PUBLIC, BLOBSTOREAPI_NO_URIS, BLOBSTOREAPI_NO_AZUREBLOBSTORECLIENT, BLOBSTOREAPI_NO_S3BLOBSTORECLIENT, and BLOBSTOREAPI_NO_ISOLATEDSTORAGEBLOBSTORECLIENT.

    Aside: Actually, BLOBSTOREAPI_PUBLIC deserves a brief note: with this release of the BlobStoreApi, the classes it implements are no longer public by default (they're expected to be consumed, not exposed). This represents a minor breaking change, but the trivial fix is to #define BLOBSTOREAPI_PUBLIC which restores things to being public as they were before. That said, it might be worth taking this opportunity to make the corresponding changes (if any) to your application - but that's entirely up to you and your schedule.

    The last thing worth mentioning is that I've tweaked BlobStoreApi to handle mixed-case blob/container names properly. Formerly, passing in upper-case characters for a blob name could result in failures for both Azure and S3; with this change in place, that scenario should work correctly.

     

    BlobStore with silly sample data

     

    New BlobStore and BlobStoreOnWindowsPhone samples

    I've updated the combined BlobStore/BlobStoreOnWindowsPhone download to include the latest version of BlobStoreApi with all the changes outlined above. Although there are no new features in either sample, they both benefit from fragmented container support and demonstrate how to use the updated API methods properly.

    [Click here to download the complete BlobStore source code and sample applications for both Silverlight 4 and Windows Phone 7.]

     

    New Delay.Web.Helpers release and updated sample

    The Delay.Web.Helpers download includes the latest version of BlobStoreApi (so it handles fragmented containers) and includes new support for container management in the form of the following methods on the AmazonS3Storage class:

    /// <summary>
    /// Lists the blob containers for an account.
    /// </summary>
    /// <returns>List of container names.</returns>
    public static IList<string> ListBlobContainers() { ... }
    
    /// <summary>
    /// Creates a blob container.
    /// </summary>
    /// <param name="containerName">Container name.</param>
    public static void CreateBlobContainer(string containerName) { ... }
    
    /// <summary>
    /// Deletes an empty blob container.
    /// </summary>
    /// <param name="containerName">Container name.</param>
    public static void DeleteBlobContainer(string containerName) { ... }

    As I discuss in the introductory post for Delay.Web.Helpers, I'm deliberately matching the existing WindowsAzureStorage API with my implementation of AmazonS3Storage, so these new methods look and function exactly the same for both web services. As part of this release, I've also updated the Delay.Web.Helpers sample page to show off the new container support as well as added some more automated tests to verify it.

    [Click here to download the Delay.Web.Helpers assembly, its complete source code, all automated tests, and the sample web site.]

     

    Summary

    While I hadn't thought to get back to blobs quite so soon, finding out about the fragmented response issue kind of forced my hand. But all's well that ends well - I'm glad to have added container supported to Delay.Web.Helpers because that means it now has complete blob-related feature parity with the WindowsAzureStorage web helper and that seems like a Good Thing. What's more, I got confirmation that the Windows Azure Managed Library does not support Silverlight, so my BlobStoreApi is serving duty as an unofficial substitute for now. [Which is cool! And a little scary... :) ]

    Whether you're using Silverlight on the desktop, writing a Windows Phone 7 application, or developing a web site with ASP.NET using MVC, Razor, or WebMatrix, I hope today's update helps your application offer just a little more goodness to your users!

    Tags: Technical Silverlight Windows Phone Web Platform
  • There's a blob on your web page - but don't wipe it off [New Delay.Web.Helpers assembly brings easy Amazon S3 blob access to ASP.NET web sites!]
    Friday, January 14th 2011

    Microsoft released a bunch of new functionality for its web platform earlier today - and all of them are free! The goodies include new tools like WebMatrix, ASP.NET MVC3 and the Razor syntax, and IIS Express - as well updates to existing tools like the Web Platform Installer and Web Deploy. These updates work together to enable a variety of new scenarios for the web development community while simultaneously making development simpler and more powerful. I encourage everyone to check it out!

     

    Web Helpers...

    One of the things I find particularly appealing is the idea of "web helpers": chunks of code that can be easily added to a project to make common tasks easier - or enable new ones! Helpers can do all kinds of things, including integrating with bit.ly, Facebook, PayPal, Twitter, and so-on. For an example of cool they are, here's how easy it is to add an interactive Twitter profile section to any ASP.NET web page [and it's even easier if you don't customize the colors :) ]:

    @Twitter.Profile(
        "DavidAns",
        height:150,
        numberOfTweets:20,
        scrollBar:true,
        backgroundShellColor:"#8ec1da",
        tweetsBackgroundColor:"#ffffff",
        tweetsColor:"#444444",
        tweetsLinksColor:"#1985b5")

    Which renders as:

    Twitter profile search

    You can see other examples of useful web helpers (including charting, cryptography, Xbox gamer cards, ReCaptcha, and more) in the ASP.NET social networking tutorial.

     

    Aside: While web helper assemblies like System.Web.Helpers and Microsoft.Web.Helpers are obviously easy to use from ASP.NET Razor and ASP.NET MVC, what's neat (and not immediately obvious) is that they don't need to be tied to those technologies. At the end of the day, helper assemblies are normal .NET assemblies and some of their functionality can be used equally well in non-web scenarios!

     

    The inspiration...

    One of the web helpers that caught my eye when I was looking around is the Windows Azure Storage Helper which makes it easy to incorporate Windows Azure blob and table access into ASP.NET applications. If you happened to be reading this blog a few months ago, you might remember that I already have some experience working with Windows Azure and Amazon S3 blobs in the form of my BlobStore Silverlight application and BlobStoreOnWindowsPhone Windows Phone 7 sample.

    When I wrote BlobStore for Silverlight, I didn't see any pre-existing libraries I could make use of (the Windows Azure Managed Library didn't (and maybe still doesn't?) work on Silverlight and there were some licensing concerns with the S3 libraries I came across), so I implemented my own support for what I needed: list/get/set/delete support for blobs on Windows Azure and Amazon Simple Storage Service (S3). But now that we're talking about ASP.NET, web helpers run on the server and the Windows Azure Storage Helper is able to leverage the Windows Azure Managed Library for all the heavy lifting so it can expose things in a clean, web helper-suitable way (via the WindowsAzureStorage static class and its associated methods).

    That's good stuff and the WindowsAzureStorage helper seems quite comprehensive - but what about customers who are already using Amazon S3 and can't (or won't) switch to Windows Azure? Well, that's where I come in. :) I'd thought it would be neat to create my own web helper assembly, and this seemed like the perfect opportunity - so I've created the Delay.Web.Helpers web helper assembly by leveraging the work I'd already done for BlobStore. This enables basic list/get/set/delete functionality for S3 blobs without a much additional effort and seems like a great way to help out more customers!

     

    The API...

    Most people don't like having many different ways to do the same thing, so I decided early on that the fundamental API for AmazonS3Storage would be the same as that already exposed by WindowsAzureStorage. That led directly to the following API (the two "secret key" properties have been renamed to match Amazon's terminology, but they're used exactly the same):

    namespace Delay.Web.Helpers
    {
        public static class AmazonS3Storage
        {
            public static string AccessKeyId { get; set; }
            public static string SecretAccessKey { get; set; }
    
            public static IList<string> ListBlobs(string bucketName);
            public static void UploadBinaryToBlob(string blobAddress, byte[] content);
            public static void UploadBinaryToBlob(string blobAddress, Stream stream);
            public static void UploadBinaryToBlob(string blobAddress, string fileName);
            public static void UploadTextToBlob(string blobAddress, string content);
            public static byte[] DownloadBlobAsByteArray(string blobAddress);
            public static void DownloadBlobAsStream(string blobAddress, Stream stream);
            public static void DownloadBlobToFile(string blobAddress, string fileName);
            public static string DownloadBlobAsText(string blobAddress);
            public static void DeleteBlob(string blobAddress);
        }
    }

    And because I wanted to be sure AmazonS3Storage could really be dropped into an application that already used WindowsAzureStorage, I did exactly that: I downloaded the WindowsAzureStorage sample application, performed a find-and-replace on the class name, updated the names of the two "secret key" properties, removed some unsupported functionality, and then verified that the sample ran and worked correctly. (It did!)

     

    The enhancements...

    While I think the WindowsAzureStorage APIs works well for listing and reading existing blobs, it's not immediately clear what a blobAddress string is made up of or how to create one, so I might get a little confused in scenarios that create blobs from scratch... It so happens that the blobAddress structure is ContainerName/BlobName (which I preserve in AmazonS3Storage), but that feels like an implementation detail developers focusing on the upload scenario shouldn't need to know. Therefore, I've also exposed the following set of methods with separate bucketName and blobName parameters (and otherwise the same signatures):

    public static void UploadBinaryToBlob(string bucketName, string blobName, byte[] content);
    public static void UploadBinaryToBlob(string bucketName, string blobName, Stream stream);
    public static void UploadBinaryToBlob(string bucketName, string blobName, string fileName);
    public static void UploadTextToBlob(string bucketName, string blobName, string content);
    public static byte[] DownloadBlobAsByteArray(string bucketName, string blobName);
    public static void DownloadBlobAsStream(string bucketName, string blobName, Stream stream);
    public static void DownloadBlobToFile(string bucketName, string blobName, string fileName);
    public static string DownloadBlobAsText(string bucketName, string blobName);
    public static void DeleteBlob(string bucketName, string blobName);

    I'm optimistic this provides a somewhat easier model for developers to work with, but I encourage folks to use whatever they think is best! :)

    Aside: I hadn't previously needed create/list/delete functionality for buckets, so the corresponding methods are not part of today's release. However, I expect them to be rather easy to add and will likely do so at a future date.

     

    The testing...

    To try to ensure I hadn't made any really dumb mistakes, I wrote a complete automated test suite for the new AmazonS3Storage class. With the exception of verifying each ArgumentNullException for null parameters, the test suite achieves complete coverage of the new code and runs as a standard Visual Studio test project (i.e., here's an example of using web helpers "off the web").

     

    The sample...

    As much fun as testing is, a sample is often the best way to demonstrate how an API is used in practice - so I also put together a (very) quick Razor-based web site to provide simple list/get/set/delete access to an arbitrary Amazon S3 account/bucket:

    Delay.Web.Helpers AmazonS3Storage sample

     

    The example...

    Using the Delay.Web.Helpers web helper is as easy as you'd expect; here's the piece of code that lists the bucket's contents (which is only as long as it is because it handles invalid credentials and empty buckets):

    @if (configured) {
        <p class="Emphasized">
            Contents of bucket <strong>@bucket</strong>:
        </p>
        var blobs = AmazonS3Storage.ListBlobs(@bucket);
        if (blobs.Any()) {
            foreach (var blobAddress in blobs) {
                <p>
                    <a href="@Href("DownloadBlob", new { BlobAddress = blobAddress})">@blobAddress.Split('/').Last()</a>
                    -
                    <a href="@Href("DeleteBlob", new { BlobAddress = blobAddress})" class="Subdued">[Delete]</a>
                </p>
            }
        } else {
            <em>[Empty]</em>
        }
    } else {
        <p>
            <strong>[Please provide valid Amazon S3 account credentials in order to use this sample.]</strong>
        </p>
    }

    The one "gotcha" that's easy to forget (and that causes compile errors when you do!) is the using statement that goes at the top of the .cshtml file and tells Razor where to find the implementation of the AmazonS3Storage class:

    @using Delay.Web.Helpers

     

    The download...

    [Click here to download the Delay.Web.Helpers assembly, its complete source code, all automated tests, and the sample web site.]

    To use the Delay.Web.Helpers assembly in your own projects, simply download the ZIP file above, unblock it (click here for directions on "unblocking" a file), extract its contents to disk, and reference Delay.Web.Helpers.dll - or copy the Delay.Web.Helpers.dll and Delay.Web.Helpers.xml files to wherever they're needed (typically the Bin directory of a web site).

    Aside: Technically, the Delay.Web.Helpers.xml file is optional - its sole purpose is to provide enhanced IntelliSense (specifically: method and parameter descriptions) in tools like Visual Studio. If you don't care about that, feel free to omit this file!

    To play around with the included sample web site, just open the folder Delay.Web.Helpers\SourceCode\SampleWebSite in WebMatrix or Visual Studio (as a web site) and run it.

    To have a look at the source code for the Delay.Web.Helpers assembly, tweak it, run the automated tests, or open the sample web site (a slightly different way), just open Delay.Web.Helpers\SourceCode\Delay.Web.Helpers.sln in Visual Studio.

     

    The summary...

    Being able to repurpose code is a great thing! In this case, all it took to convert my existing BlobStoreApi into an ASP.NET Razor/MVC-friendly web helper was writing a few thin wrapper methods. With a foundation like that, all it takes is some testing to catch mistakes, a quick sample to show it all off, and all of a sudden you've got yourself the makings of a snazzy blog post - and a new web helper! :)

    Tags: Technical Web Platform