StringCollectionEditor

Anton asked about design-time editing for the Words property (a StringCollection) of my ImageHipChallenge control.  It's a good question.

I actually intended to adorn the property with [Browsable(false)] so that the property wouldn't show up in the PropertyGrid in the designer (my original intent for this property was that in code you would open up a large dictionary file of some kind and load all of the words into collection).  But I forgot to add the attribute; oops.  The problem with adding words at design time is that you'll most likely only enter a limited set of words (since you're typing them in manually), and if the control has a small vocabulary, attacking it gets much easier for an attacker; she can visit the site manually until she knows most or all of the words your control uses, and then she can launch a programmatic attack using those discovered words.  If you didn't want to use a dictionary file, you could also generate a random word and store it into the Words collection, or you could create your own derived control and override the ChooseWord method (or just change the source for ImageHipChallenge appropriately).

However, if you still want to use Words from the designer, it's definitely possible, and only requires adding two attributes to the property:

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    [Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design",
        "System.Drawing.Design.UITypeEditor, System.Drawing")]

The DesignerSerializationVisibilityAttribute tells the designer to add initialization code for the instance of the control for all values you add to the collection.  Thus, if you add three words to the collection, you'll see something like the following in your initialization method

    this.TheControl.Words.AddRange(new string[] {"word1", "word2", "word3"});

The second attribute tells the designer to display the StringCollectionEditor when the ellipses for the property is clicked.  This editor allows you to enter one string value per line, and these are the values that will be serialized out to the AddRange method as shown above.

That works great when editing a property in the Visual Studio designer (and ends the answer to Anton's question).  However, PropertyGrid is one of my favorite components to use in my own applications as it makes writing fairly intuitive user interfaces very straightforward (and more importantly, quick!) The problem is that the above solution as described doesn't work when you bind your own PropertyGrid to an object that has a StringCollection property: attempting to edit the property by clicking the ellipses doesn't display an instance of the StringCollectionEditor, but instead displays a default CollectionEditor (and attempting to click the "Add" button in this editor will result in an error message "Constructor on type System.String not found.")

What's the problem here?  Whenever you run into a situation where you're specifying a type/assembly name and something isn't working the way you think it should (for example, specifying the type name of one collection editor and ending up with a different one), one place to look is fuslogvw.  The Assembly Binding Log Viewer utility, part of the .NET Framework SDK, allows you to see problems when fusion attempts to load an assembly and fails (it actually allows you to see more than this with a few registry key modifications).  Run fuslogvw.exe and click the "Log Failures" checkbox. Then, try running a WinForms application that binds a PropertyGrid to a class that has a public StringCollection property adorned with the EditorAttribute described above.  Hit Refresh in fuslogvw.  Sure enough, you should see an item in the listview for System.Drawing, indicating that fusion was unable to load that assembly.  Double-click on the application name to see why:

    *** Assembly Binder Log Entry  (10/12/2004 @ 11:35:37 AM) ***
    The operation failed.Bind result: hr = 0x80070002. The system cannot find the file specified.
    Assembly manager loaded from:  C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\fusion.dll
    Running under executable  C:\Documents and Settings\...\WindowsApplication1\bin\Debug\WindowsApplication1.exe--- A detailed error log follows.
    === Pre-bind state information ===
    LOG: DisplayName = System.Drawing (Partial)
    LOG: Appbase = C:\Documents and Settings\...\WindowsApplication1\bin\Debug\
    LOG: Initial PrivatePath = NULL
    LOG: Dynamic Base = NULL
    LOG: Cache Base = NULL
    LOG: AppName = NULL
    Calling assembly : System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089.
    ===

    LOG: Processing DEVPATH.
    LOG: DEVPATH is not set. Falling through to regular bind.
    LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).
    LOG: Post-policy reference: System.Drawing
    LOG: Attempting download of new URL C:/Documents and Settings/.../Debug/System.Drawing.DLL.
    LOG: Attempting download of new URL C:/Documents and Settings/.../Debug/System.Drawing/System.Drawing.DLL.
    LOG: Attempting download of new URL C:/Documents and Settings/.../Debug/System.Drawing.EXE.
    LOG: Attempting download of new URL C:/Documents and Settings/.../Debug/System.Drawing/System.Drawing.EXE.
    LOG: All probing URLs attempted and failed.

The problem is that the above EditorAttribute isn't specific enough and only specifies partial names for the assemblies.  The simple fix is to change the EditorAttribute as follows:

    [Editor(
        "System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
        "System.Drawing.Design.UITypeEditor, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]

Here, the designation for the System.Design and System.Drawing assemblies are full names rather than partial names.  Voila!  Fusion will find the assemblies in the GAC, and the String Collection Editor will work correctly in your own applications (note that the versions specified above are from the .NET Framework v1.1).