mo.notono.us

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: , , , , ,

Wednesday, December 02, 2009

SharePoint 2010: Managed Metadata fields and the TaxonomyHiddenList

Notes to self and others:

When you create a Managed Metadata (aka Taxonomy) field in a SharePoint 2010 list or library, the field’s schema will look something like this:

<field id="{f6d2b908-4ed4-42f8-a491-e1177ed57596}" version="1"
rowordinal="0" colname="int3" name="Tags" staticname="Tags"
sourceid="{58701f50-42a0-4c3d-8fdd-fb6f3a798bc4}"
enforceuniquevalues="FALSE" required="FALSE"
showfield="Term1033"
webid="956683ae-54b0-46c4-8fe6-8e14309b6fcd"
list="{4c231f71-5ed3-49aa-a62f-3fbb9a900bd0}"
displayname="Tags" type="TaxonomyFieldType">

<default>1;#A|6832dce7-bd84-4afc-8311-d5a1367dc282</default> <customization> <arrayofproperty> <property> <name>SspId</name> <value xmlns:p4="x-instance" p4:type="q1:string"
xmlns:q1="x">5dc61acc-dfa4-41dd-aa85-dd71d054ab1f</value> </property> <property> <name>GroupId</name> </property> ...

(field definition and namespaces shortened for readability)

Some of the notable attributes are:

  • Type – obvious
  • WebId - The current list’s web
  • SourceID - The current list’s id
  • List - The ID of a “TaxonomyHiddenList” which resides in the current web, but as the name implies, is hidden.
  • ShowField - the field in the TaxonomyHiddenList to display – looks like it’s locale enabled.
  • The SspId property is also crucial – it is the TermStore ID used to access the TermSet – more on that in another blog post…

The TaxonomyHiddenList’s schema looks like most lists’ schema – HUGE.  Some of the interesting fields are:

  • IdForTerm - in the case of the default value above (A) this field’s value matches the GUID 6832dce7-bd84-4afc-8311-d5a1367dc282
  • IdForTermSet - this appears to be the term set id from the MM data store – I have another column in my list which is not tied to a centrally managed metadata store – its value shows as 0000….-00… etc.
  • IdForTermStore – in my instance, this GUID is the same for both the managed and non-managed metadata field – matches the SspId above.

Labels: , ,

Tuesday, November 17, 2009

How to Upgrade a WSPBuilder Project to SharePoint 2010

<UPDATE date=”2009.12.02”>: WSPBuilder Extensions 2010 Beta 1.3 (or newer) makes the below mostly obsolete – though I was still not able to deploy using the VS add-in – I got a message stating my SPTimer Service was not running.</update>

Based on advice from Bjørn Furuknap (@furuknap, http://furuknap.blogspot.com/2009/11/using-wspbuilder-for-sharepoint-2010.html) I was able to deploy to SP 2010 a rather complex WSPBuilder-based SharePoint solution that I first recompiled in VS 2010.

Steps were:

  1. Download the command line version, with the x64 cablib dll
  2. Upgrade your VS 2008/2005 solution to VS 2010
  3. Replace your 12 references with their 14 equivalents - most will be found in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI
  4. Compile your VS solution - this SHOULD work
  5. Use the wspbuilder command line tool to create, but not deploy your wsp (use wspbuilder -help for instructions)
  6. Use stsadm -o addsolution -filename <yourwsp> to deploy the solution

Labels: , , , , ,

Monday, November 09, 2009

SharePoint Office Server 2010, on Windows 7, in Google Chrome

image

It took a few hours of downloading, a few hours of installing (with workarounds), but I now have SharePoint 2010 Office Server Beta (aka the MOSS upgrade) running on my Windows 7 laptop; and as can be seen, it renders beautifully in Google Chrome. <UPDATE1> installed and uninstalled the dang thing several times, with the same result:  I keep getting WCF errors:

System.Configuration.ConfigurationErrorsException: Unrecognized attribute 'allowInsecureTransport'. Note that attribute names are case-sensitive. (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\WebClients\Profile\client.config line 56)

</UPDATE1>

<UPDATE2>
I completely uninstalled every bit of SharePoint 2010, Visual Studio 2010, .net 4, and every prerequisite of SP.  Then reinstalled.  This time I ran the configuration wizard (as admin) while connected to the domain controller (via vpn).  MOST of SP2010 now seems to work.

I then tried adding the local SP admin account (hat I had created for my first install) to the Farm Administrators group.  Same damn WCF error.

So I simply backed up then edited client.config to remove the offensive attribute both from line 57 and from a second instance on line 97.  Then I rebooted and tried the Farm Admin Addition again.

Same error message (note that I am NOT running SPF, I am running the full Office Server version)…

image

… but different cause this time:

Local administrator privilege is required to update the Farm Administrators' group.

Could this be a limitation of running SharePoint2010 on Windows 7?
</UPDATE2>

The one Another challenge of the installation was that the Configuration wizard would fail repeatedly on step 2 (of 10) when attempting to set up the configuration database.  The error was “user can not be found”.  I then noticed that my domain account did not have full admin rights on my SQL server instance, unless I started SQL Management Studio as Run as Admin.

The workaround I used was to create a local admin account, grant that account full db access, switch to the new admin account, run the configuration wizard, and then switch back to my regular user account.  Crude, but effective.

<UPDATE2>
The better alternative is to connect to your corporate network to gain access to your domain controller.  On my 3rd, 4th and 5th (!) time running the configuration wizard, this was the approach I took, and it seems to work well.  (I understand this is also something that is necessary for the VS 2010 TFS install.)</UPDATE2>

I have no opinions on speed, etc – more of that to come, I’m sure.

<UPDATE3>
So I finally ended up scrapping Windows 7 AND 2008 R2.  With our code, there just was no getting around compatibility issues.  So for now I am running on good ol’ Windows Server 2008 x64.

Can’t wait for MS to release bootable, sysprepped developer vhds for this...
</UPDATE3>

Labels: , , , , , ,

Wednesday, November 04, 2009

This is Business Productivity?

image

Really, Microsoft?  This is the appropriate stock-photo for business productivity?

Are they productive because they are wearing suits?

Are they watching other people work productively?

Is the business being productive without them?

Did they just finish being productive and are now basking in the glow of their success?

(from http://go.microsoft.com/?linkid=9690494)

Labels: , , , , , ,

Monday, July 13, 2009

Surprise, surprise: Microsoft tries to steal Xobni's lunch

Watching the Scobleizer's interview with Microsoft Office Product Manager Chris Bryant showing new functionality in Outlook 2010 it's pretty obvious that Microsoft is not just going to let Xobni have all the fun with social networks and conversations - it will now be baked in...

Remind me why Xobni didn't take Microsoft's money when MS offered it to them?

There’s also lots of additional goodness in Outlook, let’s just hope they’ve cleaned up the ‘extra linebreak’ “function” as well.

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, May 21, 2009

Looking through the source of SharePoint on SharePoint

Microsoft launched their new SharePoint site a few days ago, and for the first time the SharePoint site is actually hosted on SharePoint (!).  It’s a nice looking site, with a dynamic user interface, courtesy of AJAX and Silverlight.  I decided to take a closer look at the visible source code – that is, the rendered HTML, JS, CSS, and Xap files.

Below are some observations:

  • They’re first loading the OOTB stylesheets, including HTML Editor and core.css (all 4K+ lines of it), completely unmodified, then they override the defaults with additional stylesheets (the MSCOMP_Core.css is another 4K+ lines of css) – seems inefficient?
  • They only load Core.js if authenticated, through a custom server control:
    <!-- RegisterCoreJSIfAuthenticated web server control -->
    <span id="ctl00_RegisterCoreJsIfAuthenticated1"></span>
  • Interestingly MS uses Webtrends
  • They use custom js to get around the name dll:
    <script type="text/javascript" src="/_catalogs/masterpage/remove_name_dll_prompt.js"></script>
  • There’s extensive Control look and feel customization through Control specific CSS
  • A lot of their stylesheets reference slwp_something – SilverLight WebPart perhaps?
  • The viewstate looks pretty nasty but in the end is only 61KB, which I guess is acceptable
  • The page includes the standard minified versions of MicrosoftAjax.js, MicrosoftAjaxWebForms.js, and SilverlightControl.js
  • The on-page Silverlight initialization code is NASTY, not sure if this is standard for Silverlight, or if this is an ugly exception.  Why not use JSON?  Why use encoded javascript?  Here’s a very short random sample – note that they didn’t bother getting rid of spaces (%20) before encoding:
    SiteNavigationDefinition%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C%2FChildSites%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3C%2FSiteNavigationDefinition%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%3CSiteNavigationDefinition%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3CSelected%3Efalse%3C%2FSelected%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3CSiteName%3EECM%3C%2FSiteName%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3CSiteUrl%3E%2Fproduct%2Fcapabilities%2Fecm%2FPages%2Fdefault.aspx%3C%2FSiteUrl%3E%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%3C
    Best of luck debugging that.
  • There’s a mix of absolute and relative references to the same image library, (but that’s a very picky observation)
  • YSlow result:  D, pinging it on number of HTTP requests, lack of a CDN (why doesn’t MS have a CDN?), Expirations headers, ETags and not minifying JS and CSS, but overall size is not bad for a MOSS page, especially not one this visually engaging – but then it turns out YSlow does not account for loading of Silverlight content – the Xap files for the Top Nav and main control are 240KB and 632KB, respectively:
    clip_image001
  • The XAP files also contain some interesting content, like this test image for the header – but they’re not actually using fast for the search…
    clip_image003
  • They use an Image Transitioner component from Advaiya (sidenote – pure Silverlight websites are just as annoying as pure Flash websites), who has supported MS on other Silverlight initiatives – wonder if the SL pieces were outsourced to them?

So – all in all a nice looking site, but I have some questions as to the completeness of the project.  Maybe it’s just me, but if I was Tony Tai (MS SharePoint Product Manager), I would spend another week finishing things up a bit…

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: , , , , ,

Friday, March 06, 2009

SharePoint 14 Wishlist

While 14 may not be coming down the pike until next year, the Alpha bits are in controlled distribution, and while I haven’t actually seen them yet, I have hopes for some major changes. 

Rather than posting my wishlist here, I thought I’d create one on Google Moderator…

See http://moderator.appspot.com/#16/e=295af – go nuts!

Labels: , , , , ,

Tuesday, February 10, 2009

MOSS: The dreaded schema.xml

My colleague Tad sent me a link to Andrew Connell’s post: A Quicker Way to Create Custom SharePoint List Templates with the comment - ‘I guess “quicker” is relative.’  Tad’s right – this seems to be going too far for a simple schema.xml file.
Much of Andrew’s post is valid – there’s not much doing getting around the Content Types and the List Template (if you need them), and using the OOTB list interface plus Keutmann’s excellent SharePoint Manager tool  to build queries is a simple workaround, but I’m balking on his approach for creating the the List’s schema.  If you take Andrew’s approach, you’re gonna end up with a MINIMUM of 1600 lines of xml in your schema: my approach is far simpler; below is a fully functioning schema that’s only 30+ lines – see if you can spot the magic bullet…:
<?xml version="1.0" encoding="utf-8"?>
<List xmlns:ows="Microsoft SharePoint" Id="{AB426CDE-98F2-432A-B296-880C7931DEF3}"
     Title="Setting" Url="Lists/Setting" BaseType="0"
     FolderCreation="FALSE" DisableAttachments="TRUE" VersioningEnabled="FALSE"
     Direction="$Resources:Direction;"
     xmlns="http://schemas.microsoft.com/sharepoint/">
       <MetaData>
              <Fields>
                     <Field Type="Text" Name="Title" DisplayName="Name" Required="TRUE" />
                     <Field Type="Text" Name="Value" DisplayName="Value" Required="TRUE" />
              </Fields>
              <Views>
                     <View BaseViewID="0" Type="HTML" WebPartZoneID="Main" DisplayName="All Items" DefaultView="TRUE"
                         MobileView="True" MobileDefaultView="False" SetupPath="pages\viewpage.aspx"
                         ImageUrl="/_layouts/images/issues.png" Url="AllItems.aspx">
                           <ViewStyle ID="17"/>
                           <RowLimit Paged="TRUE">100</RowLimit>
                           <Toolbar Type="Standard" />
                           <ViewFields>
                                  <FieldRef Name="Edit" />
                                  <FieldRef Name="Title"/>
                                  <FieldRef Name="Value"/>
                           </ViewFields>
                           <Query>
                                  <OrderBy>
                                         <FieldRef Name="Title"/>
                                  </OrderBy>
                           </Query>
                     </View>
              </Views>
              <Forms>
                     <Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
                     <Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
                     <Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
              </Forms>
              <DefaultDescription>Settings used in the application.</DefaultDescription>
       </MetaData>
</List>
Yep, it’s the ViewStyle tag I wrote about back in 2007.  It eliminates that 90% of the schema file that Andrew suggests you ignore, leaving a fairly terse xml that can actually be digested and debugged (sort of).

Labels: , ,

Tuesday, January 13, 2009

MOSS: Add Incoming Links to a Wiki Page with jQuery

Sharepoint’s wiki implementation is rudimentary, but still useful.  One of the corners cut in the implementation is that incoming links are on a separate page – you have to click the Incoming Links link (and wait for the screen to refresh) to see them.  It’d be much more user-friendly to show these links on the same page as the content.

Turns out with jQuery this is a fairly trivial exercise,  at least for a single Wiki page*:  Simply add a Content Editor Web part to the page and copy the following code into the Source Editor.

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function() {
  //get the url for the incoming links page
  u = $("a[id$=WikiIncomingLinks_LinkText]")[0].href;

  //create a target container and load it with the incoming links
  //filtered to show the links list only
  l = $("<div id='incomingLinks' style='border-top: solid 1px  silver'>").load(u + " .ms-formareaframe");

  //append the new container to the wiki content
  $(".ms-wikicontent").append(l);
});

</script>

It may be noted that the above code could even be combined into one single chain – I prefer the above for readability and debugging purposes.  Also not sure if I need to dispose of the local variables – this is a POC more than anything else.

Adding script through a CEW part

The incoming links are now on the page, right below the content:

Incoming Links directly on the Wiki Page

A more thorough implementation might position the links in a box in the upper left corner, and simultaneously removing the “Incoming Links” link.

*I haven’t quite thought out how to inject this throughout a wiki.  Any suggestions?

Labels: , , , , , ,

Monday, December 15, 2008

Re-Professing My Love of WSPBuilder

Say what you will about SharePoint as an application development platform; it adds a significant crapwork overhead to your everyday asp.net development.  Especially when it comes to organizing folder structures, creating xml files that point to other xml files, the lovely ddf syntax, etc.

WSPBuilder on the other hand…

To create a new artifact:

CropperCapture[15]

To deploy your solution (even includes an MSI installer in the Deployment Folder):

CropperCapture[18]

Labels: , , ,

Friday, December 12, 2008

Vista SP2 Includes Hyper-V…

...but you can’t actually access it:

“Hyper-V *

  • “Windows Vista SP2 includes Hyper-V™ technology, enabling full virtualization of server workloads

“*To clarify, Hyper-V is not included in Windows Vista SP2, it is part of the Windows Server 2008 service pack. This means that when you install SP2 for Windows Server 2008, or if you install a slipstreamed version of Windows Server 2008 with SP2, the RTM version of the Hyper-V role will be included. Hyper-V was released after Windows Server 2008, which means that the role you currently install is a pre-release role and needs to be updated to bring it up to RTM. This update will be applied (only if necessary) automatically when you install SP2.”

http://blogs.technet.com/springboard/archive/2008/12/02/windows-vista-sp2-what-s-inside-what-s-important.aspx

The post goes on to state that*:

“There Is Now Free Lunch*, (*to clarify: There Ain’t No Such Thing As A Free Lunch)”.

*To clarify, the post does not actually state that.

Labels: , , , ,

Monday, December 01, 2008

Installing Security Update for SQL Server Service Pack 2 (KB954606) takes a LOOOONG time

While I’m ranting, I might as well point out that installing the Security Update for SQL Server 2005 Service Pack 2 (KB954606) takes forever and a day, with no info in the “Overall Progress” bar.  That said – don’t try to cancel the update, the Cancel operation takes just as long.  So the longer you waited before you gave up on the update, the longer you’ll have to wait for the cancellation to finish.

Yay.

Labels: , , , , ,

<rant> The Bane of my VPC Existence

image

Aaaargh.  Other than simply plugging the plug on my VPC (Action – Close – Turn Off), there just doesn’t seem to be a reliable way to quickly suspend my VPC work: 

CropperCapture[9]

Since I work on an external drive with a standalone power supply I don’t dare simply suspending my laptop.

Finding that this is an issue that basically has been around since 1998 does not make me any more happy:

Still Some Bumps -- We mentioned in TidBITS-397 that changing the Mac's state, such as swapping a CD-ROM drive for a floppy drive in a PowerBook, could wreak havoc with Virtual PC's "saved state" feature. This is somewhat understandable, since when Virtual PC saves the state of your emulated PC clone for quick launching later, it has to assume that the physical machine will remain the same.

However, Virtual PC could handle these situations more gracefully. The current procedure - informing the user that the saved PC state could not be restored, and then restarting the PC - nearly guarantees the same kind of directory damage and hurt feelings that suddenly restarting a real PC would cause. The software should tell the user what's wrong and either allow an opportunity to set things right before proceeding or cancel the launch and let the user try again later with the proper hardware present, resorting to a restart only as a final option.

TidBITS, Mac News for the rest of us: Virtual PC 2.0: Not Just a Minor Upgrade

Oh – and in case you’re thinking this talks about a different product, no – it’s the same…:

Virtual PC was originally developed by Connectix for the Macintosh and was released in June 1997. In June 2001 the first version of Virtual PC for Windows, version 4.0, was released. Connectix sold versions of Virtual PC bundled with a variety of operating systems, including many versions of Windows, OS/2, and Red Hat Linux. As it became clear that virtualization was important to the enterprise, Microsoft became interested in the sector and acquired Virtual PC and an (at the time) unreleased product called "Virtual Server" from Connectix in February 2003. 

Wikipedia, Microsoft Virtual PC, Version History

I’m oh so happy to see that Connectix/MS did NOTHING to address this in the 8-9 years between v2 and v2007.

</rant>

Labels: , , , , ,

Tuesday, November 25, 2008

TypeMock offers Unit Testing Framework for SharePoint

First a disclosure:  This is a blatantly self-serving post: I want to be one of 50 recipients of a free copy of “Isolator for SharePoint”.

Mandatory statement:

Typemock are offering their new product for unit testing SharePoint called Isolator For SharePoint, for a special introduction price. it is the only tool that allows you to unit test SharePoint without a SharePoint server. To learn more click here.

The first 50 bloggers who blog this text in their blog and tell us about it, will get a Full Isolator license, Free. for rules and info click here.

But that said, I am intrigued by two things:  Roy Osherove is now doing SharePoint development?  That can’t hurt the community.  Second, I heard rumors this will work on a non-SharePoint server.  Could my VPC days be coming to an end?  (Couldn’t come fast enough…)

Labels: , , , , , ,