May 2010 Entries
MVC 2 Editor Template for Radio Buttons

A while back I blogged about how to create an HTML Helper to produce a radio button list.  In that post, my HTML helper was “wrapping” the FluentHtml library from MvcContrib to produce the following html output (given an IEnumerable list containing the items “Foo” and “Bar”):

   1:  <div>
   2:      <input id="Name_Foo" name="Name" type="radio" value="Foo" /><label for="Name_Foo" id="Name_Foo_Label">Foo</label>
   3:      <input id="Name_Bar" name="Name" type="radio" value="Bar" /><label for="Name_Bar" id="Name_Bar_Label">Bar</label>
   4:  </div>

With the release of MVC 2, we now have editor templates we can use that rely on metadata to allow us to customize our views appropriately.  For example, for the radio buttons above, we want the “id” attribute to be differentiated and unique and we want the “name” attribute to be the same across radio buttons so the buttons will be grouped together and so model binding will work appropriately. We also want the “for” attribute in the <label> element being set to correctly point to the id of the corresponding radio button.  The default behavior of the RadioButtonFor() method that comes OOTB with MVC produces the same value for the “id” and “name” attributes so this isn’t exactly what I want out the the box if I’m trying to produce the HTML mark up above.

If we use an EditorTemplate, the first gotcha that we run into is that, by default, the templates just work on your view model’s property. But in this case, we *also* was the list of items to populate all the radio buttons. It turns out that the EditorFor() methods do give you a way to pass in additional data. There is an overload of the EditorFor() method where the last parameter allows you to pass an anonymous object for “extra” data that you can use in your view – it gets put on the view data dictionary:

   1:  <%: Html.EditorFor(m => m.Name, "RadioButtonList", new { selectList = new SelectList(new[] { "Foo", "Bar" }) })%>

Now we can create a file called RadioButtonList.ascx that looks like this:

   1:  <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
   2:  <%
   3:      var list = this.ViewData["selectList"] as SelectList;
   4:  %>
   5:  <div>
   6:      <% foreach (var item in list) {
   7:             var radioId = ViewData.TemplateInfo.GetFullHtmlFieldId(item.Value);
   8:             var checkedAttr = item.Selected ? "checked=\"checked\"" : string.Empty;
   9:      %>
  10:          <input type="radio" id="<%: radioId %>" name="<%: ViewData.TemplateInfo.HtmlFieldPrefix %>" value="<%: item.Value %>" <%: checkedAttr %>/>
  11:          <label for="<%: radioId %>"><%: item.Text %></label>
  12:      <% } %>
  13:  </div>

There are several things to note about the code above. First, you can see in line #3, it’s getting the SelectList out of the view data dictionary. Then on line #7 it uses the GetFullHtmlFieldId() method from the TemplateInfo class to ensure we get unique IDs. We pass the Value to this method so that it will produce IDs like “Name_Foo” and “Name_Bar” rather than just “Name” which is our property name. However, for the “name” attribute (on line #10) we can just use the normal HtmlFieldPrefix property so that we ensure all radio buttons have the same name which corresponds to the view model’s property name. We also get to leverage the fact the a SelectListItem has a Boolean Selected property so we can set the checkedAttr variable on line #8 and use it on line #10. Finally, it’s trivial to set the correct “for” attribute for the <label> on line #11 since we already produced that value.

Because the TemplateInfo class provides all the metadata for our view, we’re able to produce this view that is widely re-usable across our application. In fact, we can create a couple HTML helpers to better encapsulate this call and make it more user friendly:

   1:  public static MvcHtmlString RadioButtonList<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, params string[] items)
   2:  {
   3:      return htmlHelper.RadioButtonList(expression, new SelectList(items));
   4:  }
   6:  public static MvcHtmlString RadioButtonList<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> items)
   7:  {
   8:      var func = expression.Compile();
   9:      var result = func(htmlHelper.ViewData.Model);
  10:      var list = new SelectList(items, "Value", "Text", result);
  11:      return htmlHelper.EditorFor(expression, "RadioButtonList", new { selectList = list });
  12:  }

This allows us to simply the call like this:

   1:  <%: Html.RadioButtonList(m => m.Name, "Foo", "Bar" ) %>

In that example, the values for the radio button are hard-coded and being passed in directly. But if you had a view model that contained a property for the collection of items you could call the second overload like this:

   1:  <%: Html.RadioButtonList(m => m.Name, Model.FooBarList ) %>

The Editor templates introduced in MVC 2 definitely allow for much more flexible views/editors than previously available. By knowing about the features you have available to you with the TemplateInfo class, you can take these concepts and customize your editors with extreme flexibility and re-usability.

Posted On Wednesday, May 26, 2010 5:02 PM | Comments (21)
MVC 2 in 2 Minutes!

In a couple of recent Code Camps, I’ve given my presentation: Top 10 Ways MVC 2 Will Boost Your Productivity. In the presentation, I cover all major new features introduced in MVC 2 with a focus on productivity enhancements. To drive the point home, I conclude with a final demo where I build a couple of screens from scratch highlighting many (but not all) of the features previously covered in the talk. A couple of weeks ago, I was asked to make it available online so here it is. In 2 minutes the demo builds a couple screens from scratch that provide a goal setting tracker for a user.

MVC 2 features included in the video are:

  • Template Helpers / Editor Templates
  • Server-side/Client-side Validation
  • Model Metadata for View Model
  • HTML Encoding Syntax
  • Dependency Injection
  • Abstract Controllers
  • Custom T4 Templates
  • Custom MVC Visual Studio 2010 Code Snippets

The complete code samples and slide deck can be downloaded here: Top 10 Ways MVC 2 Will Boost Your Productivity.


(Right-click and Zoom to view in full screen)


Click here for Direct link to video

Posted On Saturday, May 22, 2010 7:45 PM | Comments (8)
BaltoMSDN and Richmond Code Camp

Tomorrow (5/19/2010) I’ll be at BaltoMSDN presenting C# 4.0.  Also, on Saturday I’ll be presenting MVC 2 at the Richmond Code Camp.  Hope to see you there!

Posted On Tuesday, May 18, 2010 12:52 PM | Comments (1)
MVC Portable Area Modules *Without* MasterPages

Portable Areas from MvcContrib provide a great way to build modular and composite applications on top of MVC. In short, portable areas provide a way to distribute MVC binary components as simple .NET assemblies where the aspx/ascx files are actually compiled into the assembly as embedded resources. I’ve blogged about Portable Areas in the past including this post here which talks about embedding resources and you can read more of an intro to Portable Areas here.

As great as Portable Areas are, the question that seems to come up the most is: what about MasterPages? MasterPages seems to be the one thing that doesn’t work elegantly with portable areas because you specify the MasterPage in the @Page directive and it won’t use the same mechanism of the view engine so you can’t just embed them as resources. This means that you end up referencing a MasterPage that exists in the host application but not in your portable area. If you name the ContentPlaceHolderId’s correctly, it will work – but it all seems a little fragile.

Ultimately, what I want is to be able to build a portable area as a module which has no knowledge of the host application. I want to be able to invoke the module by a full route on the user’s browser and it gets invoked and “automatically appears” inside the application’s visual chrome just like a MasterPage. So how could we accomplish this with portable areas? With this question in mind, I looked around at what other people are doing to address similar problems. Specifically, I immediately looked at how the Orchard team is handling this and I found it very compelling. Basically Orchard has its own custom layout/theme framework (utilizing a custom view engine) that allows you to build your module without any regard to the host. You simply decorate your controller with the [Themed] attribute and it will render with the outer chrome around it:

   1:  [Themed]
   2:  public class HomeController : Controller

Here is the slide from the Orchard talk at this year MIX conference which shows how it conceptually works:

orchard theme


It’s pretty cool stuff.  So I figure, it must not be too difficult to incorporate this into the portable areas view engine as an optional piece of functionality. In fact, I’ll even simplify it a little – rather than have 1) Document.aspx, 2) Layout.ascx, and 3) <view>.ascx (as shown in the picture above); I’ll just have the outer page be “Chrome.aspx” and then the specific view in question. The Chrome.aspx not only takes the place of the MasterPage, but now since we’re no longer constrained by the MasterPage infrastructure, we have the choice of the Chrome.aspx living in the host or inside the portable areas as another embedded resource!

Disclaimer: credit where credit is due – much of the code from this post is me re-purposing the Orchard code to suit my needs.

To avoid confusion with Orchard, I’m going to refer to my implementation (which will be based on theirs) as a Chrome rather than a Theme. The first step I’ll take is to create a ChromedAttribute which adds a flag to the current HttpContext to indicate that the controller designated Chromed like this:

   1:  [Chromed]
   2:  public class HomeController : Controller

The attribute itself is an MVC ActionFilter attribute:

   1:  public class ChromedAttribute : ActionFilterAttribute
   2:  {
   3:      public override void OnActionExecuting(ActionExecutingContext filterContext)
   4:      {
   5:          var chromedAttribute = GetChromedAttribute(filterContext.ActionDescriptor);
   6:          if (chromedAttribute != null)
   7:          {
   8:              filterContext.HttpContext.Items[typeof(ChromedAttribute)] = null;
   9:          }
  10:      }
  12:      public static bool IsApplied(RequestContext context)
  13:      {
  14:          return context.HttpContext.Items.Contains(typeof(ChromedAttribute));
  15:      }
  17:      private static ChromedAttribute GetChromedAttribute(ActionDescriptor descriptor)
  18:      {
  19:          return descriptor.GetCustomAttributes(typeof(ChromedAttribute), true)
  20:              .Concat(descriptor.ControllerDescriptor.GetCustomAttributes(typeof(ChromedAttribute), true))
  21:              .OfType<ChromedAttribute>()
  22:              .FirstOrDefault();
  23:      }
  24:  }

With that in place, we only have to override the FindView() method of the custom view engine with these 6 lines of code:

   1:  public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
   2:  {
   3:      if (ChromedAttribute.IsApplied(controllerContext.RequestContext))
   4:      {
   5:          var bodyView = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
   6:          var documentView = ViewEngines.Engines.FindPartialView(controllerContext, "Chrome");
   7:          var chromeView = new ChromeView(bodyView, documentView);
   8:          return new ViewEngineResult(chromeView, this);
   9:      }
  11:      // Just execute normally without applying Chromed View Engine
  12:      return base.FindView(controllerContext, viewName, masterName, useCache);
  13:  }

If the view engine finds the [Chromed] attribute, it will invoke it’s own process – otherwise, it’ll just defer to the normal web forms view engine (with masterpages). The ChromeView’s primary job is to independently set the BodyContent on the view context so that it can be rendered at the appropriate place:

   1:  public class ChromeView : IView
   2:  {
   3:      private ViewEngineResult bodyView;
   4:      private ViewEngineResult documentView;
   6:      public ChromeView(ViewEngineResult bodyView, ViewEngineResult documentView)
   7:      {
   8:          this.bodyView = bodyView;
   9:          this.documentView = documentView;
  10:      }
  12:      public void Render(ViewContext viewContext, System.IO.TextWriter writer)
  13:      {
  14:          ChromeViewContext chromeViewContext = ChromeViewContext.From(viewContext);
  16:          // First render the Body view to the BodyContent
  17:          using (var bodyViewWriter = new StringWriter())
  18:          {
  19:              var bodyViewContext = new ViewContext(viewContext, bodyView.View, viewContext.ViewData, viewContext.TempData, bodyViewWriter);
  20:              this.bodyView.View.Render(bodyViewContext, bodyViewWriter);
  21:              chromeViewContext.BodyContent = bodyViewWriter.ToString();
  22:          }
  23:          // Now render the Document view
  24:          this.documentView.View.Render(viewContext, writer);
  25:      }
  26:  }

The ChromeViewContext (code excluded here) mainly just has a string property for the “BodyContent” – but it also makes sure to put itself in the HttpContext so it’s available. Finally, we created a little extension method so the module’s view can be rendered in the appropriate place:

   1:  public static void RenderBody(this HtmlHelper htmlHelper)
   2:  {
   3:      ChromeViewContext chromeViewContext = ChromeViewContext.From(htmlHelper.ViewContext);
   4:      htmlHelper.ViewContext.Writer.Write(chromeViewContext.BodyContent);
   5:  }

At this point, the other thing left is to decide how we want to implement the Chrome.aspx page. One approach is the copy/paste the HTML from the typical Site.Master and change the main content placeholder to use the HTML helper above – this way, there are no MasterPages anywhere. Alternatively, we could even have Chrome.aspx utilize the MasterPage if we wanted (e.g., in the case where some pages are Chromed and some pages want to use traditional MasterPage):

   1:  <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
   2:  <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
   3:      <% Html.RenderBody(); %>
   4:  </asp:Content>

At this point, it’s all academic. I can create a controller like this:

   1:  [Chromed]
   2:  public class WidgetController : Controller
   3:  {
   4:      public ActionResult Index()
   5:      {
   6:          return View();
   7:      }
   8:  }

Then I’ll just create Index.ascx (a partial view) and put in the text “Inside my widget”. Now when I run the app, I can request the full route (notice the controller name of “widget” in the address bar below) and the HTML from my Index.ascx will just appear where it is supposed to.

chromed page


This means no more warnings for missing MasterPages and no more need for your module to have knowledge of the host’s MasterPage placeholders. You have the option of using the Chrome.aspx in the host or providing your own while embedding it as an embedded resource itself.

I’m curious to know what people think of this approach. The code above was done with my own local copy of MvcContrib so it’s not currently something you can download. At this point, these are just my initial thoughts – just incorporating some ideas for Orchard into non-Orchard apps to enable building modular/composite apps more easily. Additionally, on the flip side, I still believe that Portable Areas have potential as the module packaging story for Orchard itself.


What do you think?

Posted On Monday, May 17, 2010 2:13 PM | Comments (6)
Version Assemblies with TFS 2010 Continuous Integration

When I first heard that TFS 2010 had moved to Workflow Foundation for Team Build, I was *extremely* skeptical. I’ve loved MSBuild and didn’t quite understand the reasons for this change. In fact, given that I’ve been exclusively using Cruise Control for Continuous Integration (CI) for the last 5+ years of my career, I was skeptical of TFS for CI in general. However, after going through the learning process for TFS 2010 recently, I’m starting to become a believer. I’m also starting to see some of the benefits with Workflow Foundation for the overall processing because it gives you constructs not available in MSBuild such as parallel tasks, better control flow constructs, and a slightly better customization story.

The first customization I had to make to the build process was to version the assemblies of my solution. This is not new. In fact, I’d recommend reading Mike Fourie’s well known post on Versioning Code in TFS before you get started. This post describes several foundational aspects of versioning assemblies regardless of your version of TFS. The main points are: 1) don’t use source control operations for your version file, 2) use a schema like <Major>.<Minor>.<IncrementalNumber>.0, and 3) do not keep AssemblyVersion and AssemblyFileVersion in sync.

To do this in TFS 2010, the best post I’ve found has been Jim Lamb’s post of building a custom TFS 2010 workflow activity. Overall, this post is excellent but the primary issue I have with it is that the assembly version numbers produced are based in a date and look like this: “2010.5.15.1”. This is definitely not what I want. I want to be able to communicate to the developers and stakeholders that we are producing the “1.1 release” or “1.2 release” – which would have an assembly version number of “1.1.317.0” for example.

In this post, I’ll walk through the process of customizing the assembly version number based on this method – customizing the concepts in Lamb’s post to suit my needs. I’ll also be combining this with the concepts of Fourie’s post – particularly with regards to the standards around how to version the assemblies.

The first thing I’ll do is add a file called SolutionAssemblyVersionInfo.cs to the root of my solution that looks like this:

   1:  using System;
   2:  using System.Reflection;
   3:  [assembly: AssemblyVersion("")]
   4:  [assembly: AssemblyFileVersion("")]

I’ll then add that file as a Visual Studio link file to each project in my solution by right-clicking the project, “Add – Existing Item…” then when I click the SolutionAssemblyVersionInfo.cs file, making sure I “Add As Link”:

Now the Solution Explorer will show our file. We can see that it’s a “link” file because of the black arrow in the icon within all our projects.

sol explorer with ver. file

Of course you’ll need to remove the AssemblyVersion and AssemblyFileVersion attributes from the AssemblyInfo.cs files to avoid the duplicate attributes since they now leave in the SolutionAssemblyVersionInfo.cs file. This is an extremely common technique so that all the projects in our solution can be versioned as a unit.

At this point, we’re ready to write our custom activity. The primary consideration is that I want the developer and/or tech lead to be able to easily be in control of the Major.Minor and then I want the CI process to add the third number with a unique incremental number. We’ll leave the fourth position always “0” for now – it’s held in reserve in case the day ever comes where we need to do an emergency patch to Production based on a branched version.


Writing the Custom Workflow Activity

Similar to Lamb’s post, I’m going to write two custom workflow activities. The “outer” activity (a xaml activity) will be pretty straight forward. It will check if the solution version file exists in the solution root and, if so, delegate the replacement of version to the AssemblyVersionInfo activity which is a CodeActivity highlighted in red below:

version xaml


Notice that the arguments of this activity are the “solutionVersionFile” and “tfsBuildNumber” which will be passed in. The tfsBuildNumber passed in will look something like this: “CI_MyApplication.4” and we’ll need to grab the “4” (i.e., the incremental revision number) and put that in the third position. Then we’ll need to honor whatever was specified for Major.Minor in the SolutionAssemblyVersionInfo.cs file. For example, if the SolutionAssemblyVersionInfo.cs file had “” for the AssemblyVersion (as shown in the first code block near the beginning of this post), then we want to resulting file to have “”.

Before we do anything, let’s put together a unit test for all this so we can know if we get it right:

   1:  [TestMethod]
   2:  public void Assembly_version_should_be_parsed_correctly_from_build_name()
   3:  {
   4:      // arrange
   5:      const string versionFile = "SolutionAssemblyVersionInfo.cs";
   6:      WriteTestVersionFile(versionFile);
   7:      var activity = new VersionAssemblies();
   8:      var arguments = new Dictionary<string, object> { 
   9:                          { "tfsBuildNumber", "CI_MyApplication.4"},
  10:                          { "solutionVersionFile", versionFile}
  11:      };
  13:      // act
  14:      var result = WorkflowInvoker.Invoke(activity, arguments);
  16:      // assert
  17:      Assert.AreEqual("", (string)result["newAssemblyFileVersion"]);
  18:      var lines = File.ReadAllLines(versionFile);
  19:      Assert.IsTrue(lines.Contains("[assembly: AssemblyVersion(\"\")]"));
  20:      Assert.IsTrue(lines.Contains("[assembly: AssemblyFileVersion(\"\")]"));
  21:  }
  23:  private void WriteTestVersionFile(string versionFile)
  24:  {
  25:      var fileContents = "using System.Reflection;\n" +
  26:          "[assembly: AssemblyVersion(\"\")]\n" +
  27:          "[assembly: AssemblyFileVersion(\"\")]";
  28:      File.WriteAllText(versionFile, fileContents);
  29:  }


At this point, the code for our AssemblyVersion activity is pretty straight forward:

   1:  [BuildActivity(HostEnvironmentOption.Agent)]
   2:  public class AssemblyVersionInfo : CodeActivity
   3:  {
   4:      [RequiredArgument]
   5:      public InArgument<string> FileName { get; set; }
   7:      [RequiredArgument]
   8:      public InArgument<string> TfsBuildNumber { get; set; }
  10:      public OutArgument<string> NewAssemblyFileVersion { get; set; }
  12:      protected override void Execute(CodeActivityContext context)
  13:      {
  14:          var solutionVersionFile = this.FileName.Get(context);
  16:          // Ensure that the file is writeable
  17:          var fileAttributes = File.GetAttributes(solutionVersionFile);
  18:          File.SetAttributes(solutionVersionFile, fileAttributes & ~FileAttributes.ReadOnly);
  20:          // Prepare assembly versions
  21:          var majorMinor = GetAssemblyMajorMinorVersionBasedOnExisting(solutionVersionFile);
  22:          var newBuildNumber = GetNewBuildNumber(this.TfsBuildNumber.Get(context));
  23:          var newAssemblyVersion = string.Format("{0}.{1}.0.0", majorMinor.Item1, majorMinor.Item2);
  24:          var newAssemblyFileVersion = string.Format("{0}.{1}.{2}.0", majorMinor.Item1, majorMinor.Item2, newBuildNumber);
  25:          this.NewAssemblyFileVersion.Set(context, newAssemblyFileVersion);
  27:          // Perform the actual replacement
  28:          var contents = this.GetFileContents(newAssemblyVersion, newAssemblyFileVersion);
  29:          File.WriteAllText(solutionVersionFile, contents);
  31:          // Restore the file's original attributes
  32:          File.SetAttributes(solutionVersionFile, fileAttributes);
  33:      }
  35:      #region Private Methods
  37:      private string GetFileContents(string newAssemblyVersion, string newAssemblyFileVersion)
  38:      {
  39:          var cs = new StringBuilder();
  40:          cs.AppendLine("using System.Reflection;");
  41:          cs.AppendFormat("[assembly: AssemblyVersion(\"{0}\")]", newAssemblyVersion);
  42:          cs.AppendLine();
  43:          cs.AppendFormat("[assembly: AssemblyFileVersion(\"{0}\")]", newAssemblyFileVersion);
  44:          return cs.ToString();
  45:      }
  47:      private Tuple<string, string> GetAssemblyMajorMinorVersionBasedOnExisting(string filePath)
  48:      {
  49:          var lines = File.ReadAllLines(filePath);
  50:          var versionLine = lines.Where(x => x.Contains("AssemblyVersion")).FirstOrDefault();
  52:          if (versionLine == null)
  53:          {
  54:              throw new InvalidOperationException("File does not contain [assembly: AssemblyVersion] attribute");
  55:          }
  57:          return ExtractMajorMinor(versionLine);
  58:      }
  60:      private static Tuple<string, string> ExtractMajorMinor(string versionLine)
  61:      {
  62:          var firstQuote = versionLine.IndexOf('"') + 1;
  63:          var secondQuote = versionLine.IndexOf('"', firstQuote);
  64:          var version = versionLine.Substring(firstQuote, secondQuote - firstQuote);
  65:          var versionParts = version.Split('.');
  66:          return new Tuple<string, string>(versionParts[0], versionParts[1]);
  67:      }
  69:      private string GetNewBuildNumber(string buildName)
  70:      {
  71:          return buildName.Substring(buildName.LastIndexOf(".") + 1);
  72:      }
  74:      #endregion
  75:  }


At this point the final step is to incorporate this activity into the overall build template. Make a copy of the DefaultTempate.xaml – we’ll call it DefaultTemplateWithVersioning.xaml. Before the build and labeling happens, drag the VersionAssemblies activity in. Then set the LabelName variable to “BuildDetail.BuildDefinition.Name + "-" + newAssemblyFileVersion since the newAssemblyFileVersion was produced by our activity.

tfs version templ


Configuring CI

Once you add your solution to source control, you can configure CI with the build definition window as shown here. The main difference is that we’ll change the Process tab to reflect a different build number format and choose our custom build process file:

build process tab


When the build completes, we’ll see the name of our project with the unique revision number:

tfs completed builds


If we look at the detailed build log for the latest build, we’ll see the label being created with our custom task:

 tfs build log


We can now look at the history labels in TFS and see the project name with the labels (the Assignment activity I added to the workflow):

 tfs labels

Finally, if we look at the physical assemblies that are produced, we can right-click on any assembly in Windows Explorer and see the assembly version in its properties:

assembly props


Full Traceability

We now have full traceability for our code. There will never be a question of what code was deployed to Production. You can always see the assembly version in the properties of the physical assembly. That can be traced back to a label in TFS where the unique revision number matches. The label in TFS gives you the complete snapshot of the code in your source control repository at the time the code was built. This type of process for full traceability has been used for many years for CI – in fact, I’ve done similar things with CCNet and SVN for quite some time. This is simply the TFS implementation of that pattern. The new features that TFS 2010 give you to make these types of customizations in your build process are quite easy once you get over the initial curve.

Posted On Saturday, May 15, 2010 8:00 AM | Comments (30)
C# 4.0 Presentation on Channel 9

My C# 4.0 presentation recorded at the Philly.NET Code Camp was posted on Channel 9 today. You can view it here.

Posted On Wednesday, May 12, 2010 4:19 PM | Comments (0)
CMAP Code Camp MVC 2 - Code Samples

Thanks to everyone who attended my MVC 2 presentation today at CMAP Code Camp. The code samples and PowerPoint can be downloaded here: Top 10 Ways MVC 2 Will Boost Your Productivity.

Posted On Saturday, May 8, 2010 8:06 PM | Comments (3)
CMAP Spring Code Camp

This Saturday is the CMAP Spring Code Camp. I’ll be presenting “Top 10 Ways MVC 2 Will Boost Your Productivity.” You can register here. I hope to see you there!

Posted On Wednesday, May 5, 2010 8:55 PM | Comments (0)

View Steve Michelotti's profile on LinkedIn

profile for Steve Michelotti at Stack Overflow, Q&A for professional and enthusiast programmers

Google My Blog

Tag Cloud