Development Blog

July 30 2010

Integrating Tridion with Team Foundation Server

Posted 15:00 | by zhaph

I thought I'd start a new series focusing on what I'm currently working on - Tridion from SDL.

The key thing I'm trying to solve at the moment is the deployment of work from one environment to another - initially one developer machine to another, then onto a shared build/deployment setup, then on through QA, User Acceptance Testing and onto live.

We've already done similar things with SharePoint, so we have a baseline of what we're trying to achieve, so it shouldn't be completely impossible.

Seeing as we're working on our C# code, CSS, JavaScript, etc in Visual Studio, tracking our tasks and bugs in TFS, and a build process built around this that takes these assets, compiles them, and generates some scripts to be run on the various environments to deploy them, we felt we'd like to try and utilise this for Tridion elements as well.

Development CMs to TFS to build and deploy servers

The first thing we tried, apparently the "recommended" way, was to use Content Porter. We wanted to prepare a "base" image that we could easily roll out to developers to start working on. We spent the better part of two days trying to break down the solution into small enough packages that we could reliably import them without it throwing an error and rolling back the last hours attempt.

We felt that not only was this frustrating, but also it wouldn't scale nicely - it relies on the developers remembering exactly what they've changed, manually exporting them out with CP - I'm sure it's possible but it does leave a large margin of error.

What we came up with was the following "ideal plan", following a standard "Good, Better Best" approach:

 

Good
(For deployment to QA)

Better

Best
(For deployment to live)

Good, Better, Best Tridion to TFS
Manual Steps

Existing content: Checkout in TFS, edit where appropriate.

New content: Add to TFS via WebDav.

Check in pending changes (including associating work items).

Check in pending changes (including associating work items). Check in pending changes (including associating work items).
Build Solution
For Automation
Automate the creation of TFS workspaces so processes can "Get Latest" straight from TFS into Tridion via WebDav.

Hook into Tridion's Pre-Save event to check-out TFS version, or add to TFS. Warn user of potential issues

Automate the creation of publications in Tridion.

We've started this, and hit our first stumbling block - some content appears to be coming back from webdav as UTF8, while some it coming back as UTF16 - which seems to be confusing TFS when doing a check in, throwing up an error such as:

Visual Studio

z:\300 Design\Building Blocks\System\btn_conference.gif.gif: TF203083: The checksum value of the file that you uploaded does not match the provided value.

Which generally went away if I tried to check the file in a second time.

This seems to be more of a problem with TFS than Tridion or WebDav - an old post from Buck Hodges "How TFS Version Control determines a file's encoding" seems to hint at difficulties with encoding, especially around:

Unfortunately, TFS does not support changing the encoding of a pending add.  If you need to do that, you will have to undo the pending add, and then re-add the file using the command line and specify the /type option.

We think we've found a solution - using a different mechanism to map the WebDav folder to a network drive seems to result in TFS seeing consistent encodings. so this feels like a good place to stop, and report back on progress later.

July 15 2010

Further work on Seesmic Desktop

Posted 22:56 | by zhaph

So, with Seemic Desktop 2 hitting Beta this week, I thought I'd just let you all know that I've updated my plugin, Doodle Grouper to work with the current version.

Since the last version, talked about in "Playing with Seesmic Desktop", I've tidied up the interfaces a bit, so they are more in keeping with the main shell theme. This was actually a fairly simple process, as simple as binding the style of the ChildWindow declaration to the main theme:

<controls:ChildWindow x:Class="DoodleGrouper.View.TempModifyGroupWindow" [...]
                      Style="{StaticResource CustomMessageBox}">

And that's it, pretty much, nice, grey dialogs, with pretty buttons on them. The only other thing I had to do was ensure that the Foreground colours of my text blocks were white so that they showed up:

Modify Group Dialog

So to sum up the changes:

  • The plugin now inherits the theme from the parent shell.
  • Groups can be renamed.
  • You can now add and remove users from a group - simple click on the Settings icon for a group, select the users you want to remove from the group, and press "Update", no additional tweets will be shown in that group, and next time you open Seesmic Desktop all their updates will be gone.
  • You can add users to the group at any time, no need to wait for them to post - just add them in, and the next time they post they'll appear - ideal for those infrequent posters that you really want to flag up.

Next on my list to consider is a way to enable popup toast on a per-group basis.

This plugin has two main advantages over the "user lists" feature in Seemic:

  1. It's not tied to just Twitter accounts - you can add Facebook and Google Buzz users in there too, in fact any account that shows posts on the "home" timeline should appear in the group timeline.
  2. It will auto-update - Twitter lists don't (didn't? I've not tried them much recently) update automatically, because each one needs to be requested individually, and this will eat into your api usage quite quickly. As these are updated as posts arrive, they don't use up that precious resource.

If you want to get your hands dirty:

  • The source - you'll need to update the references to the Seesmic.Sdp.* libraries.
  • Doodle Grouper (a repeat of the link to the compiled plug-in from above)

As always, let me know how you get on with it.

June 29 2010

Editing Reports in TFS 2010

Posted 16:14 | by zhaph

More TFS fun and games:

Team Foundation Server 2010 will happily install and configure Reporting Server for you to power all your reporting needs, but I've come up against the following issue when attempting to modify the existing reports, or create new ones:

When you click on "Report Builder" from the Reporting Services web interface, it opens up the click once application "Report Builder 1.0".

This application cannot open the reports that come with TFS process templates, and that are installed on Reporting Services - the error it reports is:

Cannot open this item
This report cannot be opened in Report Builder.

And if you check the details you see:

System.IO.StreamReader: The Report element was not found.

Which is odd, seeing as if I open the .rdl file in a text editor, I can clearly see the root Report element in there.

If you get the latest (at the time of writing 3.0) version of Report Builder, it will not connect to Reporting Services - it keeps claiming that either the services aren't configured, or I don't have permissions.

However, if you install version 2 of Report Builder, almost all your woes go away, and you can open and edit reports to your hearts content.

The only outstanding woe I had was when I opened a report from the server, edited it, and saved it and then my report appeared to break, claiming that the TfsOlapReportDS was invalid. To resolve that, I had to switch to the Properties tab of the report in the browser, select DataSources and re-map the report to the correct shared data source - once I'd done that, all future saves worked fine.

June 15 2010

Rebuilding TFS Portals

Posted 11:29 | by zhaph

We're in the process of moving to Team Foundation Server 2010, and as part of the process, we're updating the process template we're using, creating branded versions of the default documents, etc. Also, with new projects kicking off we're able to really sort out which process template we're using.

However, while we're ironing these changes out, we obviously need to actually do some work - so to that end we'd tweaked a template, created our project using that, and in the background we can work on the template for other projects.

This has left us with two things:

  1. A number of projects in our test Project Collection with portals in the wrong place in SharePoint.
  2. A number of projects using out of date portals.

At the moment, we've not made any changes to the contents in SharePoint (beyond access rights), we're mostly just using the dashboards and reports, so the cost of recreating these templates isn't that high however I was having a lot of difficulty working out how to move existing portals, or how to rebuild a portal after creating the project - the New Team Project wizard includes the option "Do not configure a SharePoint site at this time", with no real guidance on how to create one later.

As this is currently looking at WSS, and these aren't publishing sites, I can't move them easily in SharePoint. We tried exporting them, and importing them into the new location, but that also didn't work - errors about a template missing from the _forms folder (I guess the default template for the excel reports document library).

I tried creating a new site in SharePoint using the TFS templates supplied, but that didn't pick up my changes to the template, nor set up/create the shared documents, or the project dashboard. I also tried creating a dummy project with a portal in the right place, disconnecting the portal in TFS, then pointing the right project at the portal and finally deleting the dummy project, which worked, but was going to be a lot of work for all the other projects.

Finally, I took a look at the actions included in the tfpt command line tool from the Team Foundation Server Power Tools and noticed:

addprojectportal Add or move portal for an existing team project

Pass this the path to TFS, the project and process template you want to move, and optionally the details for the SharePoint instance you want to use and away it goes, creating a new Project Portal for you with all the modifications you've made.

I would point out the following issue I've had with this tool, but it may be due to my usage of it: when I pointed this at a project that already had a portal, I expected the tool to move the portal for me, however (probably because the project was built using a different Process Template) I now have two portals for this project, the old one, and the new one, not a big deal, I can easily delete the old portal - TFS seems to be happily pointing at the new one.

Running it without the /verbose option seemed to fail - and the logs weren't very helpful. Running the same command with /verbose resulted in a successful run. I may have just been unlucky.

Morals of this post:

  • Always check the power tools first.
  • MS have a habit of not finishing things, and releasing the bits you actually need as power tools.
  • Mario Rodriguez rocks.

June 09 2010

Working with TypeKit

Posted 11:10 | by zhaph

Something I meant to include in yesterday's post was around the steps I'd taken to get TypeKit working with the new GoogleApi WebFont Loader hosted version.

As I'm not using Google AJAX api  decided to just call the file directly, but I couldn't seem to get it to work using the docs on GoogleApi using the WebFontConfig object, however following the directions on the Git hub seemed to work better:

Call the webfont.js file:

<script type="text/javascript"
        src="http://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js"></script>

Then call load on the WebFont object:

  WebFont.load({
    typekit: { id: 'gjb1flq' }
    });

Passing in your TypeKit Id, (not the Kit Id as is implied in the docs).

To reduce the "Flicker of Unstyled Text" I then added the following to my CSS:

.wf-loading h1 { visibility: hidden; }
.wf-loading h2 { visibility: hidden; }
.wf-loading h3 { visibility: hidden; }
.wf-loading #logo p { visibility: hidden; }
.wf-loading code { visibility: hidden; }
.wf-loading pre { visibility: hidden; }
.wf-active { visibility: visible; }

Basically, as the web fonts are loading, the WebFont.js applies the .wf-loading style to html element, then once they are all in, applies the .wf-active style.

Rather than following the guidance on the git hub page (which suggests having the base state of the h1 hidden, and only showing it when the fonts are active) I've gone down this route, so if the script fails to load, or is turned off, the titles, code, etc will still show, albeit in their default fonts.

If I really wanted to I guess I could implement font based activation as well, using the .wf-adelle1adelle2-n7-active event.

June 08 2010

Upgrading to ASP.NET 4.0

Posted 22:00 | by zhaph

Bit of a Link List this one, but it's got a few nuggets in there too.

Thanks to my excellent web hosts LiquidSix Hosting who upgraded our servers within about a week of ASP.NET 4.0 coming out, I've been able to focus on pulling this site over to the latest platform.

There were various reasons for wanting to do this, from cleaner web.config files, the new <%: %> syntax for HTML encoding output, and indeed answering some of my issues with Entity Framework, but obviously the key reason is it's new and shiney Grin

The key issue I had with it was that I forgot to update one small part of my web.config:

<system.web>
    <httpRuntime requestValidationMode="2.0"/>
</system.web>

As I'd forgotten to set this, I was unable to use any markup when editing my blogs. Thankfully the error message tells you exactly what to set, so the fix was easy.

Other tweaks that also went live this week:

  • TypeKit have recently opensourced their code, and added an events system, which means that in FireFox the Flicker of Unstyled Text should be somewhat less intrusive.
  • The launch of DoodleStatic to host my static files (JS, CSS, images) a small, cookieless domain to speed up download of the site infrastructure.
  • Changing my calls to jQuery and Typkit's core libraries to ajax.GoogleApis.com, so your browser should be able to find them in its cache where other sites have used them.
  • Thanks to Damien Gaurd for spending the time to point out how to use the System.ServiceModel.Syndication namespaces in the context of an ASP.NET MVC site, something I'd been meaning to do for some time.

And a few other minor tweaks to keep things tidy.

June 04 2010

Notes on upgrading to Office 2010

Posted 17:22 | by zhaph

This is one of those "20/20 hindsight is a wonderful thing" posts.

The basic issue is that Visio and Project aren't really part of Office, and should probably be uninstalled before you upgrade the main Office Suite - don't rely on their installers to properly clean them out.

I didn't do this, and had the following symptoms floating around:

Visio 2007 and Project 2007 were both still listed in my All Programs menu, with a tool tip "Installs on first use".

I imagine that at somepoint after upgrading Visio 2007 to 2010, I typed Visio<enter> into my start menu, and Visio 2007 reinstalled itself. I then opened Visio 2010, and that reconfigured itself through an installer. I uninstalled Visio 2007 again from the "Programs and Features" control panel, deleted the links, and the files in C:\Windows\Installers\ that they were pointing at.

I thought I was happy - turns out I was wrong. I was working in SharePoint, and getting these odd javascript library not registered errors:

Message: Library not registered.
Line: 1935
Char: 4
Code: 0
URI: http://server/_layouts/1033/init.js?rev=ck%2BHdHQ8ABQHif7kr%2Bj7iQ%3D%3D

Running the Repair tool (Programs and Features, Office 2010, Change and select "Repair") and restarting my machine seems to have fixed the issue nicely.

I hope this helps somebody else, and saves them the hour or so of confusion it's caused me.

May 05 2010

Playing with Seesmic Desktop

Posted 17:16 | by zhaph

Update: I've uploaded a newer version, for more details see: Further work on Seesmic Desktop 05/07/2010


I've just spent a couple of evenings playing around with the Seesmic Developer Platform - an early release of their Silverlight 4 based, out-of-browser Social Media Client Seesmic Desktop.

In terms of history, I'd started with the Adobe Air based Seesmic Desktop, moved to Seesmic for Windows as soon as it was announced, and missed some of the features, but none of the resource hogging of the Air version.

The latest version of Seesmic Desktop seems like it will be able to address many of my issues - and if it doesn't, well, then it's built with extensibility in mind - the Seesmic team have taken one of the key additions to Silverlight to heart - Seesmic Desktop 2 uses the Managed Extensibility Framework, or MEF, to enable developers to easily create plugins.

With that in mind, here's the beginnings of the fruits of my labours - a plug-in for Seesmic Desktop that allows you to group users into columns that auto-update as new tweets/updates come in - yes, I know, the previous versions can all do this, and I'm sure the Seesmic guys will add it soon enough, but I always find that having a goal in mind drives me to learn about stuff more than just hacking around with no real direction.

This is currently version 0.0.1 - so it's very raw around the edges:

  • I need to work out how to pick up the parent's themes correctly (the buttons are nicely themed, but the forms aren't.
  • There's no way through the UI to remove a user from a group.
  • There's no way through the UI to remove a group.

Certainly the last two will be resolved fairly shortly Wink

You can download:

  • The source - you'll need to update the references to the Seesmic.Sdp.* libraries.
  • The compiled plugin - Save to Documents\Seesmic\Seesmic Desktop 2\Plugins

Thanks go out to Tim Heuer for his Seesmic Developer Templates.

As a basic overview, this project uses:

  • A global TimelineItemAction to allow the user to add someone to a group.
  • A TimelineItemProcessor to deliver items to the correct timelines/columns/groups.
  • SidebarActions to provide access to each group.
  • The StorageService to save group information.
  • The LogService to write rather a lot of information to the logs - I assume at some point we'll be able to say "don't write out Info level logging" or some such, but while we're all debugging this is invaluable.

The log file can be found in Documents\Seesmic\Seesmic Desktop 2\Logs and you get quite a bit of detail about loading, etc for free.

As ever, let me know how you get on, or come and join us all making it better.

April 26 2010

Implementing Flickr.Net

Posted 15:27 | by zhaph

Flickr.NET is a ".NET library for accessing the Flickr API". It's hosted on CodePlex, and (thankfully for me) updated to version 3.0 Beta just before I downloaded it.

I host all my images here on the site, provide RSS feeds of both the latest photos and each album, however beyond using a service like twitterfeed I'd not found a nice way to integrate it into other social networks - Facebook can only accept one blog feed directly and while Google Buzz picked up on the blog feed through the "Connected Sites" options (I guess through the WebMaster tools association) it only seems to accept the first feed it finds, so I thought if I post the images to Flickr as well, then there would be more integration for them.

So, after spending a little time nosing around the Flickr API docs, I went off and got the latest version of Flickr.NET, dropped it in my projects /bin folder, and added a reference to it.

The next step was to request an API key from Flickr - you'll want a different key for each application you produce as the query limits are per key - and then set up the application - you need to give it a description, pick your application type (Desktop, Web or Mobile) and then define how you're handling authentication, for a web application this means supplying a callback url - while testing http://localhost/ addresses (with port numbers if required) worked fine.

Flickr.NET comes with it's own configuration section that you could use to set up Flickr.NET:

<flickrNet apiKey="APIKEY" secret="SHAREDSECRET" cacheDisabled="true">

Which then allows you to call the library as:

var flickr = new FlickrNet.Flickr();

However, I found that this didn't play nicely in a Medium Trust environment (the main disadvantage of shared hosting), as the constructor threw an exception complaining that the it couldn't write to the cache location (even though it's configured to be disabled) - I'll admit now that I didn't look too hard into diagnosing this issue - I'm only submitting images, rather than downloading them, so I didn't feel much need for caching.

So I pulled the API Key and Shared Secret out into the main appSettings settings, and factored out the constructor into a simple factory class:

public class FlickrControl
{
  internal static Flickr GetFlickr() {
    // Disable Cache before calling anything else
    Flickr.CacheDisabled = true;

    // Create Flickr instance with simple constructor.
    var flickr = new Flickr(WebConfigurationManager.AppSettings["FlickrApi"],
                            WebConfigurationManager.AppSettings["FlickrSecret"]);

    return flickr;
  }
}

I then created two new controllers - one to handle the initial login request, and one to handle the response back from Flickr:

// User wants to authenticate with Flickr
public ActionResult Flickr() {
  // Store calling page in session to return the user to later
  Session["Referrer"] = null != Request.UrlReferrer ?
                       Request.UrlReferrer.PathAndQuery : string.Empty;

  // Get an instance of Flickr
  var flickr = FlickrControl.GetFlickr();

  // Redirect the user to Flickr authentication service
  // asking for Delete priviledges (so we can remove images).
  return new RedirectResult(flickr.AuthCalcWebUrl(AuthLevel.Delete));
}

// Flickr is returning a logged in user
public ActionResult FlickrReturn(string frob) {
  var flickr = FlickrControl.GetFlickr();

  // Generate an Auth Token from the "frob" returned from Flickr
  var auth = flickr.AuthGetToken(frob);

  // Store the authentication token for later use
  // ExternalSites.FlickrAuth is a static string value for finding this object.
  Session[ExternalSites.FlickrAuth] = auth;

  // See if we can find the users previous request to return them to.
  var referrer = Session["Referrer"] as string;

  if (!string.IsNullOrEmpty(referrer)) {
    // We found their previous page, bounce them back
    return new RedirectResult(referrer);
  }

  return RedirectToAction("Index");
}

Then when a user uploads, edits or deletes an image from the site, we check to see if they are authenticated with Flickr, and perform the same action on Flickr:

// Inside the Upload action
// Check to see if the user is Authenticated, and that they want to upload the image
if (null != Session[ExternalSites.FlickrAuth] && editPhoto.UploadToExternal) {
  var auth = Session[ExternalSites.FlickrAuth] as Auth;

  if (auth != null) {
    var flickr = FlickrControl.GetFlickr();
    // Add the user's auth token to our Flickr instance
    flickr.AuthToken = auth.Token;

    // See below
    FlickrControl.UploadImage(photo, flickr, ImageData.FileName);

    // Upload image adds the Flickr id to the photo, so we need to save
    // that to the image as well.
    m_PhotoRepository.Save();
  }
}

This is the method for uploading an image:

internal static void UploadImage(DoodlePhoto photo, Flickr flickr, string fileName) {
  if (null == photo.PhotoDetail) {
    // If editing a photo, the image won't be in memory, so needs to be loaded.
    photo.PhotoDetailReference.Load();
  }

  if (null == photo.Album) {
    // If editing a photo, the Album info won't be in memory, so needs to be loaded.
    photo.AlbumReference.Load();
  }

  // UploadPicture returns the id of the image on Flickr
  photo.FlickrId = flickr.UploadPicture(
              // Raw stream of image bytes
              new MemoryStream(photo.PhotoDetail.BytesOriginal.ToArray()),
              // might as well pass in string.Empty here,
              // not really needed as we're passing in a stream
              fileName,
              // The title that's displayed in Flickr
              photo.Caption,
              // The description that appears under the image in Flickr
              photo.Description,
              // The tag list
              photo.CollapsedTags,
              // Set the permissions on the Album - if it's public,
              // whether Family or Friends can see it.
              photo.Album.IsPublic, true, true,
              // What type of image it is (they are effectively all photos).
              ContentType.Photo,
              // I'm not uploading anything inappropriate
              SafetyLevel.Safe,
              // What search level should be used
              photo.Album.IsPublic
                ? HiddenFromSearch.Visible
                : HiddenFromSearch.Hidden);

  // Once the image is uploaded, we need to add it to the a photo set as well.
  addPhotoToPhotoset(photo, flickr);
}

To update an existing image on Flickr I use the following method:

internal static void UpdateImage(DoodlePhoto photo, Flickr flickr) {
  // Update the metadata for an existing image
  flickr.PhotosSetMeta(photo.FlickrId, photo.Caption, photo.Description);
  // Replace all tags for an existing image
  flickr.PhotosSetTags(photo.FlickrId, photo.CollapsedTags);

  // Check to see if we've created this photoset already
  if (string.IsNullOrEmpty(photo.Album.FlickrPhotoSetId)) {
    // Photo set doesn't exist, therefore we can safetly add the image
    addPhotoToPhotoset(photo, flickr);
  } else {
    // Photoset already exists, so we need to check if the image is
    // not already in the photoset, and add it if needed
    if (!photoSetContainsPhoto(photo, flickr)) {
      addPhotoToPhotoset(photo, flickr);
    }
  }
}

To add a photo to a photoset:

private static void addPhotoToPhotoset(DoodlePhoto photo, Flickr flickr) {
  // Check to see if we've already created a photoset
  if (String.IsNullOrEmpty(photo.Album.FlickrPhotoSetId)) {
    // Create a new Photoset, with the Photo's FlickrId as the Primary Photo Id
    var photoset = flickr.PhotosetsCreate(photo.Album.Caption,
                                          photo.Album.Description,
                                          photo.FlickrId);

    // Store the Photoset Id with the Album.
    photo.Album.FlickrPhotoSetId = photoset.PhotosetId;
  } else {
    // Simply add the Photo to the Photoset.
    flickr.PhotosetsAddPhoto(photo.Album.FlickrPhotoSetId, photo.FlickrId);
  }
}

Finally, the code to check if a Photo already exists in a PhotoSet:

private static bool photoSetContainsPhoto(DoodlePhoto photo, Flickr flickr) {
  // Get the details of the photos in a Photoset, will become an issue
  // only when I add more than 500 images to a Photoset!
  var photos = flickr.PhotosetsGetPhotos(photo.Album.FlickrPhotoSetId);
  // Return true if the photo's Id appears in the list.
  return null != photos.SingleOrDefault(p => p.PhotoId == photo.FlickrId);
}

Deleting a photo from Flickr is a trivial exercise, simply requiring a call to PhotosDelete with the Flickr Id of the photo.

All in all, I found this to be very clean, and fairly intutive - especially if you take the time to read the API as you go.

March 26 2010

Implementing DataAnnotations

Posted 00:29 | by zhaph

As I hinted at in my post "Upgrading to ASP.NET MVC 2.0", one of the main remaining changes I wanted to make after upgrading to version 2.0 was to move to using the DataAnnotations that gained first class support.

I've now done this, which meant that I've removed all the code based on the Nerd Dinner validation patterns, and switched to using the Validation Attributes classes.

I started with the basic RequiredAttribute to ensure that all required fields had a value, but then wanted to do a bit more - on my Contact form for example wanted to ensure that the name supplied was at least 3 characters long, that the email address used my rather more rigorous regex than ScottGu's, and that the message was at least 10 characters long.

A simple answer was to use the RegularExpressionAttribute, with settings such as @".{3,}" for the minimum length, and the lengthy pattern for emails, however I wasn't happy with this for a number of reasons:

  1. I wanted to use the "minimum length" validator on multiple properties, with different minimum lengths but very similar error messages including details of the length.
  2. Because my email pattern is compiled using string.Format to make it more manageable, I can't use it in a attribute as it needs to be a "compile time constant".

I also wanted something similar to the IntegerValidator in the Configuration namespace that allows me to specify just a MinValue, rather than the Range Validator, where I needed to supply an arbitrary high value to meet my needs.

As I'm not yet using the .NET 4 framework, I don't have access to the CustomValidatorAttribute, that's no bad thing in my mind, as I'm not a big fan of the "Magic String" form of configuration that I would need to use.

To that end I've created three validators:

public class IsEmailAttribute : RegularExpressionAttribute
{}
public class MinValueAttribute : ValidationAttribute
{}
public class MinLengthAttribute : ValidationAttribute
{}

IsEmailAttribute just supplies the email pattern (built in a read only property) to the base constructor, while MinValueAttribute and MinLengthAttribute both implement the IsValid method, and override the FormatErrorMessage to enable me to include the set value of the validator.

The full code for the MinLengthAttribute is:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class MinLengthAttribute : ValidationAttribute
{
  public int MinLength { get; set; }

  public MinLengthAttribute(int minLength) {
    MinLength = minLength;
  }

  public override bool IsValid(object value) {
    bool isValid = false;

    var stringValue = value as string;

    if (null != stringValue
        && MinLength <= stringValue.Length) {
      isValid = true;
    }

    return isValid;
  }

  public override string FormatErrorMessage(string name){
    return string.Format(ErrorMessageString, MinLength, name);
  }
}

The one big problem I've had with doing it this way is that the automation of the client-side validation doesn't work, but as it doesn't plug into the Validation Summary area, this is no great loss, as I'd have to redesign my forms more than I'd like to implement that.

The other major change I had to make to my code was to move to more of a ViewModel pattern - on some of my admin screens I was taking a FormsCollection rather than an explicit model which allowed me to have a photo form with text boxes for Caption, Order and Tags (which holds a comma seperated list of tags), but this doesn't map nicely to the Photo model, where Tags is a collection of Tag models. Writing a ViewModel for editing photos, all the annotations wired up nicely, and gave me much better control of what was being sent to the view.

One thing that was needed uploading photos that I did need to do was handle the case of a user not including an image - thankfully, the previous code set me in good stead:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UploadPhoto(int id,
                                EditPhoto editPhoto,
                HttpPostedFileBase ImageData) {
  ViewData["Title"] = "Create Photo";
  ViewData["Message"] = "Upload a new photo";

  if (ModelState.IsValid
      && null != ImageData
      && ImageData.ContentLength != 0) {
    // Persist to database
    // Return appropriate view based on Request.IsAjaxRequest
  }

  if (null == ImageData || ImageData.ContentLength != 0) {
    // ImageData is missing
    ModelState.AddModelError("ImageData", "You must supply a photo.");
  }

  // Return appropriate view based on Request.IsAjaxRequest
}

So we can still add additional errors to the ModelState property, allowing us to highlight additional fields that aren't part of the ViewModel.

Where I haven't created a ViewModel, I've used the MetadataTypeAttribute on the partial classes created for the Entity Framework classes:

[MetadataType(typeof(AlbumMetadata))]
public partial class Album {
  public string Description {
    get {
      return string.Format(
        "Photos from the album \"{0}\" on http://www.doodle.co.uk/Albums.aspx/{1}/{2}",
        Caption, AlbumId, Caption.CreateSlug());
    }
  }
}

public class AlbumMetadata {
  [Required(ErrorMessage = "You must supply a caption that is at least 3 characters long.")]
  [MinLength(3, ErrorMessage = "The caption must be at least {0} characters long.")]
  public string Caption { get; set; }
}

Next time: Implementing FlickrNet.