mo.notono.us

Friday, June 11, 2010

How to Always Run Visual Studio As Administrator

To jum straight to the solution, click here

“Certain tasks, including debugging and creating local IIS applications, require that you start Visual Studio as a user with Administrative privileges. On Windows Vista, and Windows Server 2008 when not running as the built-in Administrator account, this requires right-clicking the Visual Studio 2008 icon in the Start Menu and choosing Run as administrator.

“To make this process easier, you can create a shortcut and check the Run this program as an administrator check box on the Compatibility tab of the shortcut properties.”
from Using Visual Studio 2008 with IIS 7 @ learn.iis.net

On the last few projects I’ve worked on, we’ve used IIS sites for our development (for a number of reasons I won’t detail here), and the need to open VS in admin mode has been a constant annoyance.  It’s like constantly getting bitten by a mosquito. Today I finally got annoyed enough to spend 5 minutes researching a solution.  (I know.  I procrastinate.)

The solution, or what seems to be working for me so far at least was found at sevenforums.com: How to Run a Program as an Administrator in Windows 7.  Some of these options I knew about, the one I hadn’t tried and which worked for me was this:

1. Right click on the program shortcut or program .exe file, then click on Properties, and on the Compatibility tab. (See screenshots below)
NOTE: If you are doing this while logged on as a standard user instead of an administrator, then you will need to also click on the Change settings for all users button and type in the administrator's password.

Run as Administrator-compatibility_mode1.jpgRun as Administrator-compatibility_mode2.jpg

2. To Always Run this Program as an Administrator -

A) Check the Run this program as an administrator box, and click on OK. (See screenshots above)

The key is to change the compatibility setting of the Visual Studio EXECUTABLE, not the shortcut to it.  I.e., on my laptop, I went to C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ and right-clicked devenv.exe and then proceeded as above.

I then had to add one more step – when I now clicked on a .sln file, nothing would happen.  It appears the default Open action couldn’t run, I assume, due to inadequate privileges.  To fix this, I right-clicked the .sln file, selected Open With –> Choose Default Program, and then selected Visual Studio, making sure Always use… was checked.

Presto – my .sln files now open asking to be run as admin, as do my jump list projects.

Itch scratched.

Labels: , , , , , , , ,

Thursday, April 08, 2010

Uri Properties

For the life of me I can never remember what each property of the Uri class is meant to return.  SnippetCompiler to the rescue:

public static void RunSnippet()
{
  Uri foo = new Uri("http://some.domain.com/folder/file.htm?param=val#frag");
    foreach(PropertyInfo pi in foo.GetType().GetProperties()){
      if (pi.CanRead) {
        WL("{0}: {1}", pi.Name, pi.GetValue(foo, null));
      }
    }
}

Make sure using System.Reflection; and using System.Web; are is included, and there you have it:

AbsolutePath: /folder/file.htm
AbsoluteUri: http://some.domain.com/folder/file.htm?param=val#frag
Authority: some.domain.com
Host: some.domain.com
HostNameType: Dns
IsDefaultPort: True
IsFile: False
IsLoopback: False
IsUnc: False
LocalPath: /folder/file.htm
PathAndQuery: /folder/file.htm?param=val
Port: 80
Query: ?param=val
Fragment: #frag
Scheme: http
OriginalString: http://some.domain.com/folder/file.htm?param=val#frag
DnsSafeHost: some.domain.com
IsAbsoluteUri: True
Segments: System.String[]
UserEscaped: False
UserInfo:

UPDATE:  Steve Michellotti just showed me how this works beautifully in LinqPad as well, just set the language to C# Statement(s), replace WL with Console.WriteLine and you’re good to go.

Labels: , , , ,

Friday, December 04, 2009

Getting the ContentType from an ItemAdding Event Handler

Inside an event handler for an ItemAdded event, getting the List Item’s ContentType is as easy as

var contentType = properties.ListItem.ContentType;

However, for ItemAdding, properties.ListItem is null.

Luckily, the ContentType name and Id are part of the properties.AfterProperties collection – the following will work:

SPList list = properties.List; 
string contentTypeName = properties.AfterProperties["ContentType"].ToString(); 
SPContentType contentType = list.ContentTypes[contentTypeName]; 
SPFieldCollection fields = contentType.Fields;

Between the AfterProperties field values and the content type field collection, you typically have all that you need... Just remember to not depend on properties.ListItem.

Labels: , , , , ,

Friday, July 31, 2009

ASP.NET MVC: TagBuilder Extension Methods

I wasn’t happy with the TagBuilder class, so I improved it… See gist below:

This kind of thing could be useful in MOSS WebPart development as well…

Labels: , , , , , , , ,

Tuesday, June 16, 2009

Simple JavaScript string tokenizer

A crude String.Format equivalent in JavaScript -blatantly copied from frogsbrain

//from http://frogsbrain.wordpress.com/2007/04/28/javascript-stringformat-method/ 
String.format = function(text) { 
    if (arguments.length > 1) { 
        for (var i = 0; i < arguments.length - 1; i++) { 
            text = text.replace(new RegExp("\\{" + i + "\\}", "gi"), arguments[i + 1]); 
        } 
    } 
    return text; 
}; 

#twitcode version (130 characters!)

strf=function(t){a=arguments;if(a.length>1)for(i=0;i<a.length-1;i++)t=t.replace(new RegExp("\\{"+i+"\\}","gi"),a[i+1]);return t}; 

Labels: , ,

Thursday, June 04, 2009

My contributions to SPExLib

I just discovered Wictor Wilen (et al)’s excellent CodePlex project: SharePoint Extensions Lib, which aims to simplify SharePoint development on .NET 3.5 SP1 and up.

Since I never took the time to figure out how exactly to contribute directly to a CodePlex project, I instead first did an ad-hoc contribution of my (or rather Rob Garrett’s really) SPlist.AddItemOptimized() method.  I then quickly went through the methods that are there so far in SPExLib, and culled from my own library those that overlapped.  Below is the filtered result: these are extension methods not currently in the SPExlib project (as of June 04 2009).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;

namespace SPExLib.SharePoint
{
  #region Site Extensions
  /// <summary>
  /// SPSite Extension Methods
  /// </summary>
  public static class SiteExtensions
  {

    /// <summary>
    /// Gets a built in site field (column).
    /// </summary>
    /// <param name="site">The site.</param>
    /// <param name="builtInFieldId">The built in field id.</param>
    /// <returns>The Built In <see cref="SPField"/>.</returns>
    public static SPField GetBuiltInField(this SPSite site, Guid builtInFieldId)
    {

      return site.RootWeb.Fields[builtInFieldId];
    }

    /// <summary>
    /// Gets a <see cref="List{SPList}"/> of all lists contained within the site collection, including those in the root web and all the sub sites.
    /// </summary>
    /// <param name="site">The site.</param>
    /// <returns>A <see cref="List{SPList}"/> containing all the lists.</returns>
    public static List<SPList> GetAllLists(this SPSite site)
    {
      List<SPList> allLists = new List<SPList>();
      for (int i = 0; i < site.AllWebs.Count; i++)
      {
        using (SPWeb web = site.AllWebs[i])
        {
          allLists.AddRange(web.Lists.Cast<SPList>());
        }
      }
      return allLists;
    }
  }
  #endregion

  #region Web Extensions
  /// <summary>
  /// Extensions to the SPWeb class
  /// </summary>
  public static class WebExtensions
  {
    /// <summary>
    /// Gets the relative URL of the web.
    /// </summary>
    /// <param name="web">The web.</param>
    /// <returns>The Url of the web, relative to the site.</returns>
    public static string GetRelativeUrl(this SPWeb web)
    {
      string siteUrl = web.Site.Url;
      string webUrl = web.Url;
      if (webUrl.Contains(siteUrl))
        return webUrl.Replace(siteUrl, "");
      return null;

    }


    /// <summary>
    /// Gets the list from the list's URL, relative to the Web (which is the url used to create the list).
    /// </summary>
    /// <param name="web">The web.</param>
    /// <param name="listUrl">The list URL, relative to the web (equals the url used to create the list).</param>
    /// <returns>A <see cref="SPList"/> instance, or null, if the list was not found.</returns>
    public static SPList GetListFromListUrl(this SPWeb web, string listUrl)
    {
      SPList result = null;
      listUrl = string.Format("{0}/{1}", web.Url, listUrl);
      try
      {
        result = web.GetList(listUrl);
      }
      catch { }
      return result;
    }
  }
  #endregion

  #region SPList Extensions
  /// <summary>
  /// SPList Extension Methods
  /// </summary>
  public static class ListExtensions
  {
    /// <summary>
    /// Adds a content type to the list, optionally adding the content type fields to the default view for the list.
    /// </summary>
    /// <param name="list">The list.</param>
    /// <param name="contentType">The content type.</param>
    /// <param name="addColumnsToDefaultView">if set to <c>true</c> add columns to the list's default view.</param>
    /// <returns>The <see cref="SPContentType"/> instance.</returns>
    public static SPContentType AddContentType(this SPList list, SPContentType contentType, bool addColumnsToDefaultView)
    {
      if (contentType == null || list.ContentTypes.ContainsContentType(contentType.Id))
        return contentType;

      contentType = list.ContentTypes.Add(contentType);
      if (addColumnsToDefaultView)
      {
        bool viewUpdated = false;
        SPView defaultView = list.DefaultView;
        //SPViewFieldCollection viewFields = list.DefaultView.ViewFields;
        //Add content type fields (but only those not hidden, and also not ContentType and Title)
        foreach (SPField field in contentType.Fields)
        {
          if (!defaultView.ViewFields.Contains(field.InternalName)
            && !field.Hidden && !contentType.FieldLinks[field.Id].Hidden
            && !"|Title|ContentType|Name|".Contains(field.InternalName)
            )
          {
            defaultView.ViewFields.Add(field);
            viewUpdated = true;
          }
        }
        if (viewUpdated)
        {
          defaultView.DefaultView = true;
          defaultView.Update();
        }
      }
      list.Update();
      return contentType;
    }
  #endregion
    #region SPDocumentLibrary

    /// <summary>
    /// Adds the content type to the document library, optionally adding the content type fields to the default view for the list.
    /// </summary>
    /// <param name="documentLibrary">The document library.</param>
    /// <param name="contentType">The content type.</param>
    /// <param name="addColumnsToDefaultView">if set to <c>true</c> [add columns to default view].</param>
    /// <returns>The <see cref="SPContentType"/> instance.</returns>
    public static SPContentType AddContentType(this SPDocumentLibrary documentLibrary, SPContentType contentType, bool addColumnsToDefaultView)
    {
      return ((SPList)documentLibrary).AddContentType(contentType, addColumnsToDefaultView);
    }
    #endregion
    #region SPListCollection
    /// <summary>
    /// Creates a list based on the specified title, description, URL, Feature ID, template type, document template type, and options for displaying a link to the list in Quick Launch.
    /// Overloads and corrects the parameter types of the default Add method
    /// </summary>
    /// <param name="listCollection">The list collection.</param>
    /// <param name="title">The title.</param>
    /// <param name="description">The description.</param>
    /// <param name="url">The URL.</param>
    /// <param name="featureId">The feature id.</param>
    /// <param name="templateType">The template type.</param>
    /// <param name="docTemplateType">The doc template type.</param>
    /// <param name="quickLaunchOptions">The quick launch options.</param>
    /// <returns>A GUID that identifies the new list.</returns>
    public static Guid Add(this SPListCollection listCollection, string title, string description, string url, Guid featureId,
      SPListTemplateType templateType, int docTemplateType, SPListTemplate.QuickLaunchOptions quickLaunchOptions)
    {
      return listCollection.Add(title, description, url, featureId.ToString("B").ToUpper(),
        (int)templateType, docTemplateType.ToString(), quickLaunchOptions);
    }
    #endregion
    #region SPViewFieldCollection
    /// <summary>
    /// Determines whether the view field collection contains a field of the specified internal name.
    /// </summary>
    /// <param name="viewFieldCollection">The view field collection.</param>
    /// <param name="internalFieldName">Internal name of the  field.</param>
    /// <returns>
    ///   <c>true</c> if the view field collection contains the field; otherwise, <c>false</c>.
    /// </returns>
    public static bool Contains(this SPViewFieldCollection viewFieldCollection, string internalFieldName)
    {
      return viewFieldCollection.Cast<string>().Any(f => f == internalFieldName);
    }

    /// <summary>
    /// Adds a specified field to the collection.
    /// </summary>
    /// <param name="col">The collection.</param>
    /// <param name="fieldGuid">The field's Guid.</param>
    public static void Add(this SPViewFieldCollection col, Guid fieldGuid)
    {
      col.Add(col.View.ParentList.Fields[fieldGuid]);
    }

    /// <summary>
    /// Deletes the specified field from the collection.
    /// </summary>
    /// <param name="col">The collection.</param>
    /// <param name="fieldGuid">The field's Guid.</param>
    public static void Delete(this SPViewFieldCollection col, Guid fieldGuid)
    {
      col.Delete(col.View.ParentList.Fields[fieldGuid]);
    }

  }
    #endregion

  #region SPContentType (and related classes) extensions
  /// <summary>
  /// SPContentType and related class extensions
  /// </summary>
  public static class ContentTypeExtensions
  {
    #region SPFieldLinkCollection Extensions
    /// <summary>
    /// Adds a field to the <see cref="SPFieldLinkCollection"/>.
    /// </summary>
    /// <param name="fieldLinkCollection">The field link collection.</param>
    /// <param name="field">The field.</param>
    public static void AddField(this SPFieldLinkCollection fieldLinkCollection, SPField field)
    {
      fieldLinkCollection.Add(new SPFieldLink(field));
    }

    /// <summary>
    /// Clears the fields in a <see cref="SPFieldLinkCollection"/>.
    /// </summary>
    /// <param name="fieldLinkCollection">The field link collection.</param>
    public static void ClearFields(this SPFieldLinkCollection fieldLinkCollection)
    {
      for (int i = fieldLinkCollection.Count - 1; i >= 0; i--)
      {
        fieldLinkCollection.Delete(fieldLinkCollection[i].Id);
      }

    }
    #endregion

    #region SPContentType Extensions
    /// <summary>
    /// Adds a field link to the <see cref="SPContentType"/>.
    /// </summary>
    /// <param name="contentType">The content type.</param>
    /// <param name="field">The field.</param>
    public static void AddFieldLink(this SPContentType contentType, SPField field)
    {
      contentType.FieldLinks.Add(new SPFieldLink(field));
    }

    /// <summary>
    /// First clears the field links collection from the content type, then re-adds all Parent content type field links.
    /// </summary>
    /// <param name="contentType">The content type.</param>
    public static void ResetFieldLinks(this SPContentType contentType)
    {
      contentType.FieldLinks.ClearFields();
      foreach (SPFieldLink fl in contentType.Parent.FieldLinks)
      {
        contentType.FieldLinks.Add(fl);
      }
    }


    /// <summary>
    /// Clears the event receivers from the content type.
    /// </summary>
    /// <param name="contentType">The content type.</param>
    public static void ResetEventReceivers(this SPContentType contentType)
    {
      for (int i = contentType.EventReceivers.Count - 1; i >= 0; i--)
      {
        contentType.EventReceivers[i].Delete();
      }
      //TODO: Does a content type inherit it's parent content type receiver?
      //contentType.EventReceivers = contentType.Parent.EventReceivers;
    }

    #endregion

    #region SPContentTypeCollection Extensions
    /// <summary>
    /// Determines whether the <see cref="SPContentTypeCollection"/> contains a content type with the specified name.
    /// </summary>
    /// <param name="contentTypeCollection">The content type collection.</param>
    /// <param name="name">The name.</param>
    /// <returns>
    ///   <c>true</c> if the content type collection contains a content type with the specified name; otherwise, <c>false</c>.
    /// </returns>
    public static bool Contains(this SPContentTypeCollection contentTypeCollection, string name)
    {
      //return contentTypeCollection.Cast<SPContentType>().Any(ct => ct.Name == name);
      return (contentTypeCollection[name] != null);
    }

    /// <summary>
    /// Determines whether the content type collection contains a content type with the specified <see cref="SPContentTypeId"/>.
    /// </summary>
    /// <param name="contentTypeCollection">The content type collection.</param>
    /// <param name="contentTypeID">The content type ID.</param>
    /// <returns>
    ///   <c>true</c> if the content type collection contains a content type with the specified <see cref="SPContentTypeId"/>; otherwise, <c>false</c>.
    /// </returns>
    public static bool ContainsContentType(this SPContentTypeCollection contentTypeCollection, SPContentTypeId contentTypeID)
    {
      //old way
      //for (int i=0; i < contentTypeCollection.Count; i++)
      //{
      //  if (contentTypeCollection[i].Id == contentTypeID)
      //    return true;
      //}
      //return false;

      //new cool way:
      //return contentTypeCollection.Cast<SPContentType>().Any(ct => ct.Id == contentTypeID);

      //simple way
      return (contentTypeCollection[contentTypeID] != null);
    }

    /// <summary>
    /// Deletes a sealed content type by unsealing and un-readonly-ing the content type before attempting to delete it.
    /// </summary>
    /// <param name="contentTypeCollection">The content type collection.</param>
    /// <param name="name">The name.</param>
    /// <returns></returns>
    public static bool DeleteSealedContentType(this SPContentTypeCollection contentTypeCollection, string name)
    {
      bool success = false;
      try
      {
        SPContentType contentType = contentTypeCollection[name];
        contentType.Sealed = false;
        contentType.ReadOnly = false;
        contentTypeCollection.Delete(contentType.Id);
        success = !contentTypeCollection.ContainsContentType(contentType.Id);
      }
      catch { }
      return success;
    }

    #endregion
  }
  #endregion

  /// <summary>
  /// Extension methods for SPFile
  /// </summary>
  public static class SPFileExtensions
  {

    /// <summary>
    /// Gets the file size as string.
    /// </summary>
    /// <param name="file">The file.</param>
    /// <returns></returns>
    public static string GetFileSizeAsString(this SPFile file)
    {
      double s = file.Length;
      string[] format = new string[] { "{0} bytes", "{0} KB", "{0} MB", "{0} GB", "{0} TB", "{0} PB", "{0} EB", "{0} ZB", "{0} YB" };
      int i = 0;
      while (i < format.Length - 1 && s >= 1024)
      {
        s = (int)(100 * s / 1024) / 100.0;
        i++;
      }
      return string.Format(format[i], s.ToString("###,###,###.##"));
    }
  }
}

Labels: , , , ,

Thursday, April 09, 2009

SharePoint Bug: Send Welcome Email for External Users Points to Internal Url

If you have extended a web application to an extranet or internet zone, and you add an external user to a group while logged in to the internal zone, then elect  to send the standard welcome email (see below), the resulting email will send links to the internal zone url, not the external zone that the users will need.

This is a BUG, or at best a VERY poorly designed feature...

The problem exists in Microsoft.SharePoint.ApplicationPages.AclInv.SendEmailInvitation()*

One of the lines read:

string urlToEncode = base.Web.Url + "/_layouts/people.aspx?MembershipGroupId=" 
  + SPHttpUtility.UrlKeyValueEncode(byID.ID.ToString());

The problem, of course is that base.Web will be the web that the current user is in, not the web that the extranet user will have rights to.

Other than replacing/editing the AclInv.aspx page in Layouts I don't see any way this can be fixed.  Given that this thread has been out here for more than two years I hold little hope that MS will fix the issue any time soon...

(*locate the dll in C:\inetpub\wwwroot\wss\Virtual Directories\<your site>\_app_bin and reflect away...)

Labels: , , , , ,

C#: Generate Readable Passwords

On a recent project I found it necessary to replace the Forms Based Authentication Membership provider generated passwords with some of my own choosing, in order to make the passwords actually readable and typeable.  Call me a usability-nut, but I consider passwords like t7(N4(Jm9{]M@@ and |NxXl&^ah;z4]- (both actual ResetPassword() result examples) to be neither readable nor typeable.

So I followed Mark Fitzpatrick's approach, which essentially is to take the output of the ResetPassword method and use it as the old password input for the ChangePassword method. But now I still needed a new password, so since nothing came out of a cursory Google search, I concocted the utility method below. Note that this is neither rocket surgery nor particularly brainy science and it may not suit your purpose, but it generates a random 9-10 character password from a set of 1.5+ million, which did the trick for me:

UPDATE: The random number generator in .NET isn’t all that random, unless you force it to be – see  GetRandomIndex method at the bottom.

UPDATE 2: Silly me – you have to apply the .Next() method of the SAME random instance, otherwise you end up with very recognizable patterns.  Removed the GetRandomIndex method and added _randomInstance member variable.

 

//Create a static instance of the Random class, on which to operate
private static Random _randomInstance = new Random();

/// <summary>
/// Generates a random, but readable, password, at least 8 characters and containing one or more 
/// lowercase and uppercase letters, numbers, and symbols.
/// </summary>
/// <returns>The random password</returns>
internal static string GenerateReadablePassword()
{
  //simple short words
  string[] words = new string[40] {
"Rocket", "Ship", "Plane", "Train", "Boat",
"Space", "Earth", "Venus", "Mars", "Moon",
"Valley", "Water", "Land", "Beach", "Field",
"Puppy", "Kitten", "Tiger", "Turtle", "Bird",
"Texas", "Maine", "Kansas", "Florida", "Alaska",
"Hand", "Head", "Foot", "Wrist", "Elbow",
"Happy", "Smile", "Grin", "Humor", "Spirit",
"Soccer", "Cricket", "Tennis", "Bowling", "Yummy"
};
  //alphabets: don't include those that can be misinterpreted for another letter/number
  string alphabets = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXY";
  //numbers: exclude 0 (~= O or o), 1 (~= I, or l) and 2 (~= z)
  string numbers = "3456789";
  //common symbols: no brackets, parentheses or slashes
  string symbols = "!@#$+=&*?";

  //put the elements together in a random format, but don't stick an alphabet immediately after a word
  string[] formats = new string[18] {
"{1}{2}{3}{0}", 
"{1}{2}{0}{3}", "{2}{3}{1}{0}", "{3}{0}{2}{1}",
"{0}{2}{3}{1}", "{1}{3}{0}{2}", "{3}{1}{2}{0}",
"{0}{2}{1}{3}", "{1}{3}{2}{0}", "{2}{0}{3}{1}", "{3}{1}{0}{2}",
"{0}{3}{1}{2}", "{1}{0}{2}{3}", "{2}{1}{3}{0}", 
"{0}{3}{2}{1}", "{1}{0}{3}{2}", "{2}{1}{0}{3}", "{3}{2}{1}{0}"
};

  //combine the elements
  string password = string.Format(GetRandomString(formats), //random format
    GetRandomString(words), //0
    GetRandomStringCharacter(alphabets), //1
    GetRandomStringCharacter(numbers), //2
    GetRandomStringCharacter(symbols) //3
  );

  //post fill the password with numbers as necessary
  while (password.Length < 8)
  {
    password += GetRandomStringCharacter(numbers);
  }

  return password;
}

/// <summary>
/// Gets the random string character.
/// </summary>
/// <param name="inputString">The input string.</param>
/// <returns></returns>
private static char GetRandomStringCharacter(string inputString)
{
  return inputString[_randomInstance.Next(0, inputString.Length)];
}

/// <summary>
/// Gets the random string.
/// </summary>
/// <param name="inputStringArray">The input string array.</param>
/// <returns></returns>
private static string GetRandomString(string[] inputStringArray)
{
  return inputStringArray[_randomInstance.Next(0, inputStringArray.Length)];
}

Labels: , , , , ,

Wednesday, March 11, 2009

ASP.NET 101 Reminder 2: ListItem[] is an array of Reference Types

..which means that if you fill one ListControl with the ListItem[] array, and then fill another ListControl with the same array, BOTH DropDownLists will be updated if you modify any ListItem (such as selecting it: ListItem.Selected = true;)

If you think of a ListControl as its HTML equivalent then this may be a bit confusing and bug-prone (and indeed, once rendered as HTML the controls are no longer in sync, it only happens server-side), but from an OO perspective it does make perfect sense.

It just makes me wish for easier and generic deep cloning of collections of objects…. Sigh.

Labels: , , , , , ,

ASP.NET 101 Reminder: ListControl.Text == ListControl.SelectedValue…

…and NOT ListControl.SelectedItem.Text

ListControl implements IEditableTextControl and thus ITextControl, which mandates the Text property.  For whatever reason (I’m sure it was a good one) the ASP.NET architects chose to make the Text property implementation get and set the ListControl.SelectedValue rather than ListControl.SelectedItem.Text.

 

Coincidentally, there is no ListControl.FindByText method: the following extension methods may come in handy:

/// <summary>
/// Performs a case insensitive comparison finding a ListItem by its text value, and selecting that item
/// </summary>
/// <param name="listControl">The list control</param>
/// <param name="text">The text to match</param>
/// <returns>The first matched, selected <see cref="SPListItem"/> or null if not found.</returns>
public static ListItem SelectFirstByText(this ListControl listControl, string text)
{
  // first clear any previous selection 
  listControl.SelectedIndex = -1;
		
  foreach (ListItem item in listControl.Items)
  {
    if (string.Equals(item.Text, text, System.StringComparison.OrdinalIgnoreCase))
    {
      item.Selected = true;
      return item;
    }
  }
  //if not found...
  return null;
}

/// <summary>
/// Performs a case insensitive comparison finding ListItems by their text value, and selecting those items
/// </summary>
/// <param name="listControl">The list control.</param>
/// <param name="text">The text to match.</param>
/// <returns>An integer array of matched indices.</returns>
public static int[] SelectByText(this ListControl listControl, string text)
{
  List matches = new List();
  // first clear any previous selection 
  listControl.SelectedIndex = -1;
  int i = 0;
  foreach (ListItem item in listControl.Items)
  {
    if (string.Equals(item.Text, text, System.StringComparison.OrdinalIgnoreCase))
    {
      item.Selected = true;
      matches.Add(i);
    }
    i++;
  }
  return matches.ToArray();
}

Labels: , , , , ,

Tuesday, March 10, 2009

Visual Studio: A handful of useful Find and Replace Expressions

  • Find empty get; set; property declarations:
    \n:b*\{:b*\n:b*get;:b*\n:b*set;:b*\n:b*\}
  • Replace empty get; set; declarations with single line equivalent:
    { get; set; }
  • Find candidates for single line summary comments:
    /// \<summary\>\n:b+///:b*{[^\n]*}:b*\n:b+/// \</summary\>
  • Replace candidates with single line equivalent:
    /// \<summary\>\1\</summary\>
  • Find Class Diagram generated get:
    get\n:b+\{\n:b+throw new System.NotImplementedException\(\);\n:b+\}
  • Find Class Diagram generated empty set:
    set\n:b+\{\n:b+\}

Labels: , , , , ,

Tuesday, July 29, 2008

SQL: Once again, for the record...

Performance is not a compelling reason to choose stored procedures over dynamic SQL.

At work today I was surprised to overhear a suggestion to dynamically create stored procedures in SQL, just "to take advantage of the added performance of stored procedures" over dynamic/ad-hoc/inline SQL.

So here we go again, for the record:

Performance of Stored Proc vs. Dynamic/Ad-hoc SQL

Actual, simplistic test by BlackWasp:

image
http://www.blackwasp.co.uk/SpeedTestSqlSproc.aspx

SQL Books On Line:

SQL Server 2000 and SQL Server version 7.0 incorporate a number of changes to statement processing that extend many of the performance benefits of stored procedures to all SQL statements. SQL Server 2000 and SQL Server 7.0 do not save a partially compiled plan for stored procedures when they are created. A stored procedure is compiled at execution time, like any other Transact-SQL statement. SQL Server 2000 and SQL Server 7.0 retain execution plans for all SQL statements in the procedure cache, not just stored procedure execution plans. The database engine uses an efficient algorithm for comparing new Transact-SQL statements with the Transact-SQL statements of existing execution plans. If the database engine determines that a new Transact-SQL statement matches the Transact-SQL statement of an existing execution plan, it reuses the plan. This reduces the relative performance benefit of precompiling stored procedures by extending execution plan reuse to all SQL statements.
http://msdn2.microsoft.com/en-us/library/aa174792(SQL.80).aspx

DeKlarit dev take:

People still thinks Stored Procedures are faster, even if there is much evidence that shows otherwise. Fortunately, when they go and ask the LinQ for SQL/Entities team they get the same answer than they get from me. They are not.
http://weblogs.asp.net/aaguiar/archive/2006/06/22/Stored-Procs-vs-Dynamic-SQL.aspx

There are lots of good reasons to use Stored Procedures, and lots of good reasons to use Dynamic SQL.  Performance is rarely a good reason to chooswe one way or the other, and if performance is  the decision factor often dynamic sql comes out on top, e.g. if you can use dynamic sql to eliminate an OR for example (as when using optional query parameters in an advanced search form).

Labels: , , ,

Tuesday, July 22, 2008

C#: String.Inject() - Format strings by key tokens

I generally prefer to use String.Format() instead of doing a bunch of string additions, but constantly find myself splitting the code onto multiple lines with an appended comment to keep track of the indices:

string myString = string.Format("{0} is {1} and {2} is {3}",
  o.foo, //0
  o.bar, //1
  o.yadi, //2
  o.yada //3
);

Wouldn't it be nice you could instead write something like this?:

string myString = "{foo} is {bar} and {yadi} is {yada}".Inject(o);

Well, now you can - see the string extension method Inject below; it accepts an object, IDictionary or HashTable and replaces the property name/key tokens with the instance values.  Since it uses string.Format internally, it also supports string.Format-like custom formatting:

   1:  using System;
   2:  using System.Text.RegularExpressions;
   3:  using System.Collections;
   4:  using System.Globalization;
   5:  using System.ComponentModel;
   6:   
   7:  [assembly: CLSCompliant(true)]
   8:  namespace StringInject
   9:  {
  10:    public static class StringInjectExtension
  11:    {
  12:      /// <summary>
  13:      /// Extension method that replaces keys in a string with the values of matching object properties.
  14:      /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
  15:      /// </summary>
  16:      /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
  17:      /// <param name="injectionObject">The object whose properties should be injected in the string</param>
  18:      /// <returns>A version of the formatString string with keys replaced by (formatted) key values.</returns>
  19:      public static string Inject(this string formatString, object injectionObject)
  20:      {
  21:        return formatString.Inject(GetPropertyHash(injectionObject));
  22:      }
  23:   
  24:      /// <summary>
  25:      /// Extension method that replaces keys in a string with the values of matching dictionary entries.
  26:      /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
  27:      /// </summary>
  28:      /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
  29:      /// <param name="dictionary">An <see cref="IDictionary"/> with keys and values to inject into the string</param>
  30:      /// <returns>A version of the formatString string with dictionary keys replaced by (formatted) key values.</returns>
  31:      public static string Inject(this string formatString, IDictionary dictionary)
  32:      {
  33:        return formatString.Inject(new Hashtable(dictionary));
  34:      }
  35:   
  36:      /// <summary>
  37:      /// Extension method that replaces keys in a string with the values of matching hashtable entries.
  38:      /// <remarks>Uses <see cref="String.Format()"/> internally; custom formats should match those used for that method.</remarks>
  39:      /// </summary>
  40:      /// <param name="formatString">The format string, containing keys like {foo} and {foo:SomeFormat}.</param>
  41:      /// <param name="attributes">A <see cref="Hashtable"/> with keys and values to inject into the string</param>
  42:      /// <returns>A version of the formatString string with hastable keys replaced by (formatted) key values.</returns>
  43:      public static string Inject(this string formatString, Hashtable attributes)
  44:      {
  45:        string result = formatString;
  46:        if (attributes == null || formatString == null)
  47:          return result;
  48:   
  49:        foreach (string attributeKey in attributes.Keys)
  50:        {
  51:          result = result.InjectSingleValue(attributeKey, attributes[attributeKey]);
  52:        }
  53:        return result;
  54:      }
  55:   
  56:      /// <summary>
  57:      /// Replaces all instances of a 'key' (e.g. {foo} or {foo:SomeFormat}) in a string with an optionally formatted value, and returns the result.
  58:      /// </summary>
  59:      /// <param name="formatString">The string containing the key; unformatted ({foo}), or formatted ({foo:SomeFormat})</param>
  60:      /// <param name="key">The key name (foo)</param>
  61:      /// <param name="replacementValue">The replacement value; if null is replaced with an empty string</param>
  62:      /// <returns>The input string with any instances of the key replaced with the replacement value</returns>
  63:      public static string InjectSingleValue(this string formatString, string key, object replacementValue)
  64:      {
  65:        string result = formatString;
  66:        //regex replacement of key with value, where the generic key format is:
  67:        //Regex foo = new Regex("{(foo)(?:}|(?::(.[^}]*)}))");
  68:        Regex attributeRegex = new Regex("{(" + key + ")(?:}|(?::(.[^}]*)}))");  //for key = foo, matches {foo} and {foo:SomeFormat}
  69:        
  70:        //loop through matches, since each key may be used more than once (and with a different format string)
  71:        foreach (Match m in attributeRegex.Matches(formatString))
  72:        {
  73:          string replacement = m.ToString();
  74:          if (m.Groups[2].Length > 0) //matched {foo:SomeFormat}
  75:          {
  76:            //do a double string.Format - first to build the proper format string, and then to format the replacement value
  77:            string attributeFormatString = string.Format(CultureInfo.InvariantCulture, "{{0:{0}}}", m.Groups[2]);
  78:            replacement = string.Format(CultureInfo.CurrentCulture, attributeFormatString, replacementValue);
  79:          }
  80:          else //matched {foo}
  81:          {
  82:            replacement = (replacementValue ?? string.Empty).ToString();
  83:          }
  84:          //perform replacements, one match at a time
  85:          result = result.Replace(m.ToString(), replacement);  //attributeRegex.Replace(result, replacement, 1);
  86:        }
  87:        return result;
  88:   
  89:      }
  90:   
  91:   
  92:      /// <summary>
  93:      /// Creates a HashTable based on current object state.
  94:      /// <remarks>Copied from the MVCToolkit HtmlExtensionUtility class</remarks>
  95:      /// </summary>
  96:      /// <param name="properties">The object from which to get the properties</param>
  97:      /// <returns>A <see cref="Hashtable"/> containing the object instance's property names and their values</returns>
  98:      private static Hashtable GetPropertyHash(object properties)
  99:      {
 100:        Hashtable values = null;
 101:        if (properties != null)
 102:        {
 103:          values = new Hashtable();
 104:          PropertyDescriptorCollection props = TypeDescriptor.GetProperties(properties);
 105:          foreach (PropertyDescriptor prop in props)
 106:          {
 107:            values.Add(prop.Name, prop.GetValue(properties));
 108:          }
 109:        }
 110:        return values;
 111:      }
 112:   
 113:    }
 114:  }

File downloads:

Labels: , , , , , , , , ,

Tuesday, June 24, 2008

.NET: What exactly does Trim() trim?

I found myself using the character specific String.Trim(params char[] trimChars) overload to remove some carriage returns and tabs when I realized I was reinventing the wheel again.  Not content with the basic Trim()'s intellisense ("Removes all occurrences of white space characters from the beginning and end of this instance") I dove into Reflector:

public string Trim()
{
    return this.TrimHelper(WhitespaceChars, 2);
}

TrimHelper is fairly obvious - it does the actual trimming of the characters.

The WhitespaceChars array (which is also used by TryParse() and some of the other TrimXxx() methods) is set in the String static constructor:

static String()
{
    Empty = "";
    WhitespaceChars = new char[] { 
        '\t', '\n', '\v', '\f', '\r', ' ', '\x0085', '\x00a0', ' ', 
        ' ', ' ', ' ', ' ', ' ', ' ', ' ', 
        ' ', ' ', ' ', ' ', '​', '\u2028', '\u2029', ' ', ''
     };
}

Not sure what some of those non-printable characters are, but I'm satisfied...

Labels: , , , ,

Thursday, January 10, 2008

LiveWriter: Testing CodeSnippet Plugin

Leo Vildosola has written a Code Snippet Plugin for Windows Live Writer.  It looks like it works quite nicely - here is the code from my Extension Methods Post:

/// <summary>
/// Creates a list based on the specified title, description, URL, Feature ID, template type, document template type, and options for displaying a link to the list in Quick Launch.
/// </summary>
/// <param name="listCollection">The list collection.</param>
/// <param name="title">The title.</param>
/// <param name="description">The description.</param>
/// <param name="featureId">The feature id.</param>
/// <param name="templateType">The template type.</param>
/// <param name="docTemplateType">The doc template type.</param>
/// <param name="quickLaunchOptions">The quick launch options.</param>
/// <returns>A GUID that identifies the new list.</returns>
public static Guid Add(this SPListCollection listCollection, string title, string description, string url, Guid featureId, 
    SPListTemplateType templateType, int docTemplateType, SPListTemplate.QuickLaunchOptions quickLaunchOptions)
{
    return listCollection.Add(title, description, url, featureId.ToString("B").ToUpper(), 
        (int)templateType, docTemplateType.ToString(), quickLaunchOptions);
}

(via Frank's World)

Labels: , , ,

C#/MOSS: Extension Methods: For Good and For Evil

Let me start with some good news:  C# 3.0/ASP.NET 3.5 works beautifully with SharePoint 2007, thanks to the forward-compatible nature of the .NET framework from version 2.0 onwards.

This compatibility opens up all sorts of avenues, one of them is the use of Extension Methods, replacing trusty old Utility classes.

Extension methods are amazingly simple, yet handy, essentially they allow you to "correct" the behavior of a class that you otherwise have no control over.  This can be used both for Good (adding useful methods to a poorly written API) and for Evil (making C# look like VB).  And of course it can also be used for sheer Anal Retentiveness:

For instance, I am not a fan of the SPListCollection.Add() method, especially the last two overloads.  Among other parameters these overloads take a string featureID(where the string represents a Guid), an integer templateType (where the integer represents a SPListTemplateType enum), and a string docTemplateType (where the string represents an integer).  WTF?  Why aren't these parameters in their native types?

So I created the following method inside a static class:

/// <summary>
/// Creates a list based on the specified title, description, URL, Feature ID, template type, document template type, and options for displaying a link to the list in Quick Launch.
/// </summary>
/// <param name="listCollection">The list collection.</param>
/// <param name="title">The title.</param>
/// <param name="description">The description.</param>
/// <param name="featureId">The feature id.</param>
/// <param name="templateType">The template type.</param>
/// <param name="docTemplateType">The doc template type.</param>
/// <param name="quickLaunchOptions">The quick launch options.</param>
/// <returns>A GUID that identifies the new list.</returns>
public static Guid Add(this SPListCollection listCollection, string title, string description, 
  string url, Guid featureId, SPListTemplateType templateType, int docTemplateType, 
  SPListTemplate.QuickLaunchOptions quickLaunchOptions)
{
  return listCollection.Add(title, description, url, featureId.ToString("B").ToUpper(), 
    (int)templateType, docTemplateType.ToString(), quickLaunchOptions);
}

Itch scratched. The properly typed overload now shows up in intellisense.

Labels: , , , , ,

Tuesday, November 20, 2007

C#: An actual use for a Predicate

I love the List<T> class in C#2.0+, but haven't really used it much beyond as a typed ArrayList.  It's normally plenty.

But today I was building a dynamic CAML string and started doing a bunch of repetitive code like

List clauses = new List();

string foo = GetFooClause();
if (!string.IsNullOrEmpty(foo))
   clauses.Add(foo);

string bar = GetBarClause();
if (!string.IsNullOrEmpty(bar))
   clauses.Add(bar);

//..
when I realized there was a much simpler way:
List clauses = new List();

clauses.Add(GetFooClause());
clauses.Add(GetBarClause());
//...			

//remove all empty clauses
clauses.RemoveAll(string.IsNullOrEmpty);

The last line is the kicker - List.RemoveAll() takes a Predicate (i.e. a functor with a single parameter) - and String.IsNullOrEmpty() happens to be just the Predicate for the job.

Labels: ,

Monday, October 08, 2007

Announcing squrl.us

Squrl.us (Short, Quick URLs) is a side project I've been putting together over some weekends and weeknights lately.  Essentially it is a tinyurl clone, but with added functionality, and a cuter look.

It's all really quite simple:  You enter a long and unruly url, press the Scurry button, and in return get a number of short, quick urls that are easy to remember, jot down, or communicate verbally.

For example, the url http://www.spiegel.de/panorama/leute/0,1518,510187,00.html is converted to

The squrl is just a redirection to the original link, the mobile link uses Skweezer.net to render a mobile-friendly version, the cache link goes to the Google cache for the url, and the translated link is to Google's automatic translation service (note this only works for non-English pages).

The whole thing is running on ASP.NET 3.5 with C# 3.0 and Linq (for the heck of it), and SQL Server Express 2005 (this may be migrated to MySQL 5.0 due to server constraints).

Go try it out!

Labels: , , , , ,

Sunday, September 16, 2007

HumanBase32: Base32 targeted for Human/Computer interaction

This morning I decided I needed a Base32 encoder/decoder for a hobby project.  Since one of the possible use-cases for this project involves a person reading large numbers, I wanted something short and compact, so adding characters to Base10 seemed logical.

However I wanted to stick with basic alphanumeric characters found on a standard US keyboard. I also didn't want to deal with case sensitivity, which rules out Base64 (0-9, a-z, A-Z, +/).   Furthermore I didn't want to deal with issues resulting from confusing similar-looking characters like i, l and 1, or o and 0.

It looked like my best option was Douglas Crockford's reasonable looking Base32 encoding scheme, which excludes the letters i,l,o and u (u is excluded as a very cursory profanity check). Unfortunately Crockford does not provide any code for his scheme.

So I set out to write my own - below is the result, which I call HumanBase32. (This does not allow for the checksum and dash options, as Crockford's article describes.)

using System;
using System.Collections.Generic;

/// <summary>
/// Human targeted Base32 encoding, as defined by Douglas Crockford (http://www.crockford.com/wrmg/base32.html)
/// </summary>
public class HumanBase32
{
  #region Private Members
  public const int Base = 32;
  private static Dictionary<char, int> _decodingDictionary = new Dictionary<char, int>(Base);
  private static Dictionary<int, char> _encodingDictionary = new Dictionary<int, char>(Base);
  /// <summary>
  /// Static constructor to fill the dictionaries
  /// </summary>
  static HumanBase32()
  {
    FillDecodingDictionary();
    FillEncodingDictionary();
  }
  /// <summary>
  /// Fills the encoding dictionary
  /// </summary>
  private static void FillEncodingDictionary()
  {
    _encodingDictionary.Add(0, '0');
    _encodingDictionary.Add(1, '1');
    _encodingDictionary.Add(2, '2');
    _encodingDictionary.Add(3, '3');
    _encodingDictionary.Add(4, '4');
    _encodingDictionary.Add(5, '5');
    _encodingDictionary.Add(6, '6');
    _encodingDictionary.Add(7, '7');
    _encodingDictionary.Add(8, '8');
    _encodingDictionary.Add(9, '9');
    _encodingDictionary.Add(10, 'a');
    _encodingDictionary.Add(11, 'b');
    _encodingDictionary.Add(12, 'c');
    _encodingDictionary.Add(13, 'd');
    _encodingDictionary.Add(14, 'e');
    _encodingDictionary.Add(15, 'f');
    _encodingDictionary.Add(16, 'g');
    _encodingDictionary.Add(17, 'h');
    _encodingDictionary.Add(18, 'j');
    _encodingDictionary.Add(19, 'k');
    _encodingDictionary.Add(20, 'm');
    _encodingDictionary.Add(21, 'n');
    _encodingDictionary.Add(22, 'p');
    _encodingDictionary.Add(23, 'q');
    _encodingDictionary.Add(24, 'r');
    _encodingDictionary.Add(25, 's');
    _encodingDictionary.Add(26, 't');
    _encodingDictionary.Add(27, 'v');
    _encodingDictionary.Add(28, 'w');
    _encodingDictionary.Add(29, 'x');
    _encodingDictionary.Add(30, 'y');
    _encodingDictionary.Add(31, 'z');
  }

  /// <summary>
  /// Fills the decoding dictionary
  /// </summary>
  private static void FillDecodingDictionary()
  {
    // o, O, and 0 should all = 0
    _decodingDictionary.Add('0', 0);
    _decodingDictionary.Add('o', 0);
    //i, I, l, L, and 1 should all = 1
    _decodingDictionary.Add('1', 1);
    _decodingDictionary.Add('i', 1);
    _decodingDictionary.Add('l', 1);
    _decodingDictionary.Add('2', 2);
    _decodingDictionary.Add('3', 3);
    _decodingDictionary.Add('4', 4);
    _decodingDictionary.Add('5', 5);
    _decodingDictionary.Add('6', 6);
    _decodingDictionary.Add('7', 7);
    _decodingDictionary.Add('8', 8);
    _decodingDictionary.Add('9', 9);
    _decodingDictionary.Add('a', 10);
    _decodingDictionary.Add('b', 11);
    _decodingDictionary.Add('c', 12);
    _decodingDictionary.Add('d', 13);
    _decodingDictionary.Add('e', 14);
    _decodingDictionary.Add('f', 15);
    _decodingDictionary.Add('g', 16);
    _decodingDictionary.Add('h', 17);
    _decodingDictionary.Add('j', 18);
    _decodingDictionary.Add('k', 19);
    _decodingDictionary.Add('m', 20);
    _decodingDictionary.Add('n', 21);
    _decodingDictionary.Add('p', 22);
    _decodingDictionary.Add('q', 23);
    _decodingDictionary.Add('r', 24);
    _decodingDictionary.Add('s', 25);
    _decodingDictionary.Add('t', 26);
    //u is excluded to prevent accidental 'f_ck's.
    _decodingDictionary.Add('v', 27);
    _decodingDictionary.Add('w', 28);
    _decodingDictionary.Add('x', 29);
    _decodingDictionary.Add('y', 30);
    _decodingDictionary.Add('z', 31);
  }

  /// <summary>
  /// Get the Base32 encode length for a given number
  /// </summary>
  /// <param name="number">Number to encode</param>
  /// <returns>Length of Base32 symbol</returns>
  private static int GetSymbolLength(long number)
  {
    //Length = Floor(Log(number, base32)) + 1
    return Convert.ToInt32(Math.Floor(Convert.ToDecimal(Math.Log(number, Base))) + 1);
  }

  /// <summary>
  /// Get the positional multiplier, where position is counted from right, starting with 1
  /// </summary>
  /// <param name="i">1-based, right-to-left position</param>
  /// <returns>Base32 positional multiplier</returns>
  private static long GetPositionalMultiplier(int i)
  {
    return Convert.ToInt64(Math.Pow(Base, i - 1));
  }
  #endregion (Private Members)
  /// <summary>
  /// Encodes a long integer to a HumanBase32 string.
  /// </summary>
  /// <param name="number">The Base10 number to encode</param>
  /// <returns>The Base32 encoded symbol string</returns>
  public static string Encode(long number)
  {
    string symbol = "";
    try
    {
      number = Math.Abs(number);
      int symbolLength = GetSymbolLength(number);
      char[] symbolArray = new char[symbolLength];
      //start with the highest symbol digit, and proceed down
      for (int i = symbolLength; i > 0; i--)
      {
        //get the positional multiplier:
        //(e.g. in base10, the number 3 in 321 has a positional multiplier of 10^(3-1) = 100)
        long positionalMultiplier = GetPositionalMultiplier(i);
        //get the symbol digit by Div'ing the number by the positional base
        //(e.g. in 321(base10), the first digit is 321 Div 100 = 3
        int symbolDigit = Convert.ToInt32(Math.DivRem(number, positionalMultiplier, out number));
        //then convert to HumanBase32 symbol
        symbolArray[symbolLength - i] = _encodingDictionary[symbolDigit];
      }
      symbol = new String(symbolArray);
    }
    catch
    {
      symbol = "(NAN)";
    }
    return symbol;
  }
  /// <summary>
  /// Decodes the HumanBase32 symbol to a long integer
  /// </summary>
  /// <param name="symbol">HumanBase32 symbol string</param>
  /// <returns>The long integer result, or -1 if an invalid symbol</returns>
  public static long Decode(string symbol)
  {
    long number = 0;
    try
    {
      symbol = symbol.Trim().ToLower();
      int symbolLength = symbol.Length;
      //loop through the characters from left to right
      for (int i = symbolLength; i > 0; i--)
      {
        //Get the character
        char symbolChar = symbol[symbolLength - i];
        //Decode the digit
        int symbolDigit = _decodingDictionary[symbolChar];
        //Get the positional multiplier for the digit
        long positionalMultiplier = GetPositionalMultiplier(i);
        //Add to the result
        number += symbolDigit * positionalMultiplier;
      }
    }
    catch
    {
      number = -1;
    }
    return number;
  }
}

Labels: ,

Thursday, February 01, 2007

Self-Updating (and Self-healing) Applications

Scott Hanselman wrote a post called Making Your Application Update Itself that inspired me to leave a rather long comment. Since I don't blog much these days, I figured I'd also post my comment here:

The way I did this at my last job was to force an update check at the launch of the application. The app was an internal WinForm 1.1 app, with a webservice backend, since the users were not all in the same location. Rather than politely asking if the users wanted to update, we actually could require that they always work in the latest release.

So, I created a separate launcher exe that checked a webservice for a list of current files: if the list of files on the server differed from that on the client (determined by the hash of each file), then it'd download the necessary files, but only those. This had several benefits: We didn't need an installer for anything other than the launcher. If we only updated a single dll, then that was the only dll that needed to be sent to the clients. And should a user be boneheaded and delete an assembly, the application would heal itself.

To speed things up we compressed and decompressed the dlls (through the same web service compression utility we used for all webservice calls over a certain size).

The whole thing worked exceptionally well and was by far one of the more enjoyable tasks on the whole project (along with the custom web service compression task)...

Labels: , , ,