Oct31

A Workaround for SharePoint Property Bug in Custom Field Types

Recently, I was working on developing custom field types and controls for SharePoint. There is some really amazing customization capability here that I am very excited to talk about in a future article, but today I want to talk about something I know many people have seen, but (so far as I know) nobody has been able to solve without a lot of code.

I am talking about a bug that occurs when you add properties to the PropertySchema element in your custom field type definition file. The way these are supposed to work is that adding these properties will give you additional settings for your field that you can access from the code of your custom field class. This is supposed to save you a lot of work by providing a UI for properties that are based on fields that already exist in SharePoint, like text or choice.

However, the properties don't work as expected. After creating a new field (either in a list or as a site column) and saving it, the data entered is indeed stored and available to you via the GetCustomProperty function of the SPField class, but when you return to make changes to the field, it's properties are not properly set and there is garbage text in the form instead.

Broken FldEditEx.aspx Page

Now, I am not really sure what is going wrong here under the hood in SharePoint. I have read some things that speculated there was a hotfix to this problem, and at least one person has speculated that such a hotfix will be rolled into Service Pack 1, whenever that is. I would've thought that the HUGE security updates from September and October that required schema updates would have rolled this fix in, but they didn't solve this issue.

Some folks have solved this issue by writing custom property type rendering controls for their fields. There is a lot of chatter on the forums about this solution and the various shortcomings with it. However, I am not a big fan of writing a bunch of code where it should not be needed, just to address some broken functionality. I wanted to be able to use the custom property schema as intended out-of-the-box to make my custom field controls configurable without a ton of extra work.

Well, I did some digging, and though it was very difficult and took me a little over a day, I found the answer to my prayers. I know Microsoft will eventually fix this issue, but in the mean time, I'm providing source code for you to download and use in your own custom field controls so that you can get around this issue. See the link at below for the code, or keep reading to find out how it works.

Note that I haven't been able to reproduce the issue reliably on every SharePoint machine. I'll leave it to Microsoft to determine the root cause and get to the bottom of it and fix it. Basically, I think you should try every other option at your disposal first, and then try this if there is nothing left that works for you

My Approach

If you attach to the debugger in Visual Studio while you are on the FldEditEx.aspx page, you will find that your own custom field type is indeed being constructed (several times in fact) as that page goes through its own lifecycle.

So, I got to thinking, "If the data in SharePoint is OK, but the data shown on the form is bad, then it must be just a rendering issue in the page and we can probably write over it as long as we do so before the page posts the 'corrupted' data back into SharePoint." I did a little digging in the debugger and I could see that at certain points, the field did indeed have the "correct" custom properties.

So far, so good. But how to get the correct data back into the web form where it belongs, and not too soon so that SharePoint doesn't then just copy over my changes? Well, you can get the page context from HttpContext.Current and actually everything you need from there is at your disposal.

I dug into the HTML source and found the text input element for my first custom property. This was easy enough since I knew the field title that appears in its label. It had an ID property like this:

ctl00_PlaceHolderMain_OptionalSettings_ctl00_ctl00_ctl01_ctl00_ctl00_TextField

After redeploying my feature, the ID actually changed slightly. It seems some of the ID properties for placeholders, templates, and such are set dynamically.

In the debugger, if you look through the Controls collections of the page object, you can see that there is a complex hierarchy of templated controls being used to render the form. Knowing that, I was able to browse the control hierarchy for the page, looking for any control that ended in "ctl00_TextField". Then I walked up the Parent properties of all the controls and was able to determine this hierarchy.

ID

Type

ctl00

ASP._layouts_application_master

ctl00

System.Web.UI.HtmlControls.HtmlGenericControl

ctl13 

System.Web.UI.HtmlControls.HtmlForm

We can get this deep with page.Form.

PlaceHolderMain

System.Web.UI.WebControls.ContentPlaceHolder

OptionalSettings ASP

Control of dynamic type

PlaceHolderControls

System.Web.UI.WebControls.PlaceHolder

Customization 

System.Web.UI.WebControls.PlaceHolder

We can get to here using a recursive FindControl command.

ctl08

Microsoft.SharePoint.WebControls.ListFieldIterator

We can loop through the Controls for a control of this type.

ctl00 

Microsoft.SharePoint.WebControls.TemplateContainer

ctl01 

Microsoft.SharePoint.WebControls.FormField

ctl00 

Microsoft.SharePoint.WebControls.TextField

ctl00 

Microsoft.SharePoint.WebControls.TemplateContainer

TextField

System.Web.UI.WebControls.TextBox

What a pain! I hate trying to access properties in templates! Half the time they don't even have static ID properties, so FindControl doesn't work. That is true in the case as well, but fortunately there is a predictable pattern that we can use to find what we need. I wrote a couple simple functions to help us do recursive searches for controls and a search by type. You can find examples of these in just about a million places on the web.

    public class WebControlTools {

        /// <summary>

        /// Used to scan for controls within a hierarchy of control containers. The

        /// </summary>

        /// <param name="root">The top levle control at which to begin the search</param>

        /// <param name="id">The ID or ClientOD property to search for</param>

        /// <param name="resolveByClientId">

        /// Set to true if you want to find a control whose CLientID property

        /// ends with a specific string. Useful for finding a specific control

        /// buried deep inside nested templates, mostly for research.</param>

        /// <returns></returns>

        public static Control RecurseFindControl( Control root, string id, bool resolveByClientId ) {

            if (resolveByClientId) {

                if (root.ClientID.EndsWith( id ))

                    return root;

                if (root.Controls == null)

                    return null;

                foreach (Control subCtl in root.Controls) {

                    Control found = RecurseFindControl( subCtl, id, resolveByClientId );

                    if (found != null)

                        return found;

                }

            } else {

                Control ctl = root.FindControl( id );

                if (ctl != null)

                    return ctl;

                if (root.Controls == null)

                    return null;

                foreach (Control subCtl in root.Controls) {

                    Control found = RecurseFindControl( subCtl, id, resolveByClientId );

                    if (found != null)

                        return found;

                }

            }

            return null;

        }

        public static Control RecurseFindControl( Control root, string id ) {

            return RecurseFindControl( root, id, false );

        }

 

        /// <summary>

        /// Gets the first child control that matches the specified type.

        /// This is useful for finding tempalted controls when you know

        /// the type but the ID is determined dynamically at run time.

        /// </summary>

        /// <param name="parent">The parent control to search</param>

        /// <param name="controlType">The type of control to search for</param>

        /// <param name="skipHowMany">An optional parameter to tell the search to skip the first x numbe rof controlls of this type that it finds, default is 0</param>

        /// <returns>A control of the specified type, or null if none found</returns>

        public static Control GetChildControlByType( Control parent, Type controlType, int skipHowMany ) {

            int i = 0;

            foreach (Control ctl in parent.Controls) {

                if (ctl.GetType() == controlType) {

                    if (i++ >= skipHowMany)

                        return ctl;

                }

            }

            return null;

        }

        public static Control GetChildControlByType( Control parent, Type controlType ) {

            return GetChildControlByType( parent, controlType, 0 );

        }

    }

It might've also been possible to get the field at a point where its FieldRenderingControl was already created and set up, and then use that to bootstrap our way up the stack of parent controls to get where we needed to be, but I haven't tried that yet.

 So anyway, the FldEditEx.aspx page has a Placeholder control, called Customization, that holds the dynamically rendered controls for - of all things - our custom properties. Once we can get that, we can grab the ListIterator control beneath it. Within that, the FormField controls represent the editor controls for each custom property.

         /// <summary>

        /// Search a Page object for the ListIterator control that acts

        /// as a container for all the custom property field controls for

        /// that page. This will only work on SharePoint's FldEditEx.aspx page.

        /// </summary>

        /// <param name="page"></param>

        /// <returns></returns>

        private Control GetListIteratorControl( Page page ) {

            /*

            Control phMain = page.Form.FindControl( "PlaceHolderMain" ); if (phMain == null) return;

            Control oSettings = phMain.FindControl( "OptionalSettings" ); if (oSettings == null) return;

            Control phControls = oSettings.FindControl( "PlaceHolderControls" ); if (phControls == null) return;

            Control custom = oSettings.FindControl( "Customization" ); if (custom == null) return null;

            */

            Control custom = WebControlTools.RecurseFindControl( page.Form, "Customization" );

            if (custom == null)

                return null;

            ListFieldIterator listIterator = WebControlTools.GetChildControlByType( custom, typeof( ListFieldIterator ) ) as ListFieldIterator;

            return listIterator;

        }

We can fix them all in one fell swoop by looping through all the controls in the ListIterator. For each FormField control, we do a GetCustomProperty and then just stuff that value into the form field's Value property. Since the design of FormField (and all its derived classes) is that Value is used to set the value of the underlying primitive web controls - Voilla!

        private void ReplaceCorruptedFieldValues( ListFieldIterator listIterator, SPField field ) {

            if (listIterator == null)

                return;

            if (listIterator.ControlMode == SPControlMode.New)

                return;

            foreach (Control propertyTemplate in listIterator.Controls) {

                // there is generally always a template for the individual field property

                foreach (Control lic in propertyTemplate.Controls) {

                    FormField ff = lic as FormField; // this is the form field for the custom property, not the actual list item field

                    if (ff != null) { // filter out any LiteralControls that are floating around the form field

                        // get the field name and do the substitution

                        string fieldName = ff.FieldName;

                        object prop = field.GetCustomProperty( fieldName );

                        ff.ItemFieldValue = prop; // this may not be necessary but it hasn't bitten me yet

                        ff.Value = prop;

                    }

                }

            }

        }

As you can see from this screen, the problem is fixed!

Fixed FldEditEx.aspx Page

One last caveat, since the field gets instantiated several times during the page lifecycle, we need a way to make sure our code runs at the correct time, and that it only runs once. I do this by creating a static method called DoHookup which takes the custom field as an argument. It does all the work of getting the page object and managing the event. If the passed field has not been sent to DoHookup before, it will bind a PreRender event to the page to do the rest of the work. To make sure we track the fields we have called, I just store a generic string list in the page's Items collection and check it every time the static method is called.  This works much better than using a static property, since ASP.net will run multiple pages in a single thread and you don't want field names to cross over the pages in this case.

     /// <summary>

    /// This class contains code to workaround an issue that prevents the properties

    /// of a custom field from being properly displayed in the FldEditEx.aspx page.

    /// </summary>

    public class CustomFieldPropertyDisplayWorkaround {

        private SPField _field = null;

        private Page _page = null;

 

        /// <summary>

        /// You can call this method as many times as you want. It will only

        /// create an event one time per unique field that is passed to it.

        /// </summary>

        /// <param name="field"></param>

        /// <returns></returns>

        public static object DoHookup( SPField field) {

            if (HttpContext.Current == null || field == null)

                return null;

            Page page = HttpContext.Current.Handler as Page;

            if (page == null || page.IsPostBack)

                return null;

 

            // ensure that we only attach a single pre render event for each unique field name

            // TODO do something to ensure that fields trigger by other parts of the form or custom

            // properties do not get added here

            List<string> fieldList = GetFieldList(page);

            if (!fieldList.Contains( field.InternalName )) {

                object work = new CustomFieldPropertyDisplayWorkaround( page, field );

                fieldList.Add( field.InternalName );

                return work;

            }

            return null;

        }

 

        private static List<string> GetFieldList(Page page) {

            List<string> fieldList = page.Items["FieldList"] as List<string>;

            if (fieldList == null) {

                fieldList = new List<string>();

                page.Items.Add("FieldList", fieldList);

            }

            return fieldList;

        }

 

        internal CustomFieldPropertyDisplayWorkaround( Page page, SPField field ) {

            this._page = page;

            this._field = field;

            _page.PreRender += new EventHandler( page_PreRender );

        } // SPField field

 

        protected void page_PreRender( object sender, EventArgs e ) {

            if (_field == null) return;

            if (_page == null || _page.IsPostBack) return;

            ListFieldIterator listIterator = GetListIteratorControl( _page ) as ListFieldIterator;

            ReplaceCorruptedFieldValues( listIterator, this._field );

        }

 

        // ... other methods shown above

 

}

To incorporate this workaround into your own custom field classes, simply call CustomFieldPropertyDisplayWorkaround.DoHookup( this ); from the constructors of your custom field. (Note, not the field value, and not the field control - the field itself). That's all you should need to do.

Keep in mind that this solution only does the workaround on fields you created (or at least have source code for), since the out-of-the-box SharePoint fields all work as designed - it's only custom properties that are hosed. If you have custom fields with custom properties in use where the author has not provided you the source, you need to tell them about this workaround. (If you are very brave, you might try to figure out how to wire up your own code on the page to an instance of their field object using events, but I don't need to do it, so I'm not gonna.)

In case you're wondering how this will affect custom properties when they're accessed outside of a web context, it won't! If any of the controls don't line up as expected, or the HttpContext is null, the code doesn't do anything at all. Since our issue is a malfunctioning web page, we don't need the workaround anyplace but here. :-)

Download the Code

You can download the needed source code here.

References and More Information

More chit-chat about this issue is in various places in the SharePoint community:

Published: Oct-31-07 | 1  Comment | 0  Link to this post

Oct28

How I Know that My Kids Are Ready to See Avenue Q

So, I'm sitting in my dining room this morning, talking to Alara and sipping at my coffee. Then this happened.
 
Alex, my three year old, comes down the stairs, saying rather matter-of-factly, "I want hepatitis and bad breath."
 
Chasing his older brother he repeats, "Give me hepatitis and bad breath!"
 
So, Eric, who is 11, is standing there with an Elmo plushie and two Giant Microbes. So, he gives him hepatitis and halatosis, and Alex is delighted. He then poses the Elmo and puppeting the doll with his best Elmo voice says, "I knew I should never have slept with Big Bird!"
 
Sometimes kids say the darndest things. That was the best twenty bucks I've spent on them since My Little Cthulu.
Published: Oct-28-07 | 0  Comment | 0  Link to this post

Oct22

InfoPath Forms "Dropdown of Repeating Self" Bug Stomp

I was fooling around with InfoPath this weekend, trying to create a generic form and data definition for a flexible ad hoc workflow.
 
One thing I wanted in this form was the ability to specify dynamic routing between workflow steps. So, I created a repeating table for the steps and I also bound a repeating section to the same repeating group in the data. This let me summarize the steps in the table and then let the user specify the properties for each step in the details section below.
 
In the details section, I have dropdowns for Approve To, Reject To, and Overdue To that are meant to tell the workflow which step it should switch to when these events take place. Each of these dropdowns is meant to be populated by the list of steps.
 
Here is a shot of the designer:
 
Screenshot of the Designer View in InfoPath
 
So, in the data the XML looks like this:
 
Workflow Data Source Schema
 
Basically, I have the dropdowns bound to RouteTo in each case, and I want them to let the user pick from ID and TItle for the entire list of WorkflowTask items.
 
This is where InfoPath barfed, showing a glaring weakness. When you bind the dropdown list's items to the form data, and specify the repeating group as //WorkflowTasks/WorkflowTask it *should* encode that as relative to the parent element, giving you the full list, but instead it encodes it relative to the current element (which turns out to be just '.') giving you only the current WorkflowTask node.
 
I should also note that if you point the list to a repeating node that is outside the parent oath of the node of your dropdown list, then it will behave itself normally and give you all the nodes under that group.
 
Though it may be okay in some cases, for a dropdown list this behavior is less than useless. The field browser should give you enough control to let you specify a differnt xpath query at least, which it does not, leaving you stuck with what the wizard lets you pick. I will completely ignore the obvious usefulness of an xpath expression complete with functions, etc. that could've easily allowed for dynamic filtering of the list items.
 
So, my workaround was to save the IP template as source files, and then go into the source for view1.xsl and find the specific xsl that does the transform for the list. In my case, this looks like this.
 
I search for "Route To", which is my display name for the dropdown. Below, look for code like:
 
<xsl:for-each select="."><-- This is the offending xpath
 <option>
  <xsl:attribute name="value">
   <xsl:value-of select="@my:ID"/>
  </xsl:attribute>
  <xsl:if test="
$val=@my:ID">
   <xsl:attribute name="selected">selected</xsl:attribute>
  </xsl:if>
  <xsl:value-of select="@my:DisplayName"/>
 </option>
</xsl:for-each>
 
So, to get the complete list of nodes when you are already inside of it, just swing up to the parent node and select all the desired sub-nodes, like so:
 
<xsl:for-each select="../my:WorkflowTask">
 
Once I am done, I open my manifest.xsf and save as back to the original xsn form template. It isn't clear to me how long this hack will hold, but I was able to preview the modified template, and even make some changes to other parts of the form without reverting to the undesirable '.'.
 
Here's the preview of the form:
 
Screenshot of InfoPath Form in Preview Mode
 
It's not the most elegant solution in terms of maintainability, but it was a lot easier than writing custom code behind (and in some cases that isn't necessarily an option anyway), so I am glad to have found it.
 
Come to think of it, though I haven't tried it yet, you could hack the select with an xpath expression to filter the dropdown. If anyone gives that a shot, shoot me an email or comment and let me know. (Send it to anything at thomascarpe.com until I get my blog comments working.)
 
Update:
I did eventually see some weird looking XSL in the dropdowns after a couple of successive load-saves. This caused the value that was selected to be displayed as a choice within the list, and was completely undesirable behavior. I simply used the same hack as above to rid myself of the annoying extra option tag. :-)
Published: Oct-22-07 | 0  Comment | 0  Link to this post

Oct15

Ahh, the Joist (and Pain) of Attic Rennovation!

So, I busted myself up pretty badly this weekend while working in the attic. Let me tell you, you can't be too careful when you're working in a attic space. It's dark, there's generally no floor, and there's often not much in the way of headroom.
 
I was reminded of this over the weekend. Eric and I went up on Saturday to take up two peices of the half finished plywood subfloor, because I wanted tog et some glue down under it before we moved any farther along. This, I figured would help to prevent squeaking floors in the future and also seal the air gaps between the joists and the floor, aiding insulation.
 
We had no trouble taking up the screws I had put down several weeks ago. The top layer of plywood split in a couple of places, but I was planning to use some wood filler anyway to smooth out the screwholes when all is said and done. I tossed the boards into the eaves as I went, and only one was a full size peice, so it wasn't too tough.
 
We decided not to take up the start run in the eaves because it will be behind the knee wall and not very likely to squeak, though I do supppose it could've benefited from the air sealing. Perhaps I'll apply a line of caulk to it before I close everything up.
 
The first sheet's glue (I use PL Premium - HoH swears on it) went on without any real trouble. The only issue was that it was dark and the glue is brown and so iwas the wood, so I wasn't really sure if I was using enough or too much. If I had it to do over I'd get better lighting. Oh, and the stuff smells a little funny, so maybe I'd turn on the vent fan too. Anyway, I applied a line or two across the tops of all the joists, then I simply grabbed the plywood and slid it over and into place. It was relatively easy to line it up just where it had been before because it was flush asgainst the gable wall and the T&G gets pounded into place along the other floorboards.
 
So, this is where I ran into a little trouble. The next board was a full board, and I had to slide out intot eh middle of the open ceiling to reach the far side. The cualk gun I used to apply the glue takes a lot of body english to squeeze the stuff out, and as I was applying a line that was difficult to reach, I slipped and knocked my little seat board, a roughly 4 ft. peice of 2x8 scrap, out from under myself. The board tumbled to the ground in the boys' bedroom and bounced around a bit before hitting the floor.
 
I did noticably less bouncing as I tumbled over on my side and barely caught myself between the joist under my knee and the one that was suddenly just under my armpit, It slammed into my side with a force I'd say is about equal to being hit in the side with a baseball bat at full swing. Actually, because it was thinner than a bat, and more square, it was really more like one of those wooden katanas they use for practice.
 
It left me reeling and I developed quite a nasty bruise from the impact, as well as a complete inability to focus on much of anything for a few days. Of course pain makes concentration more difficult. I can only take so much ibuprophen before my stomach rebels.
 
Nevertheless, I already had glue drying, so I had to finish what I was doing before I could even stop. I got the rest of the glue for the peice on, tooka  short break, and then muscled through the remiaining smaller board. I didn't accomplish any more work fro the rest of the day after that.
 
Fortunately, the next day I was able to muster up some energy and cut through the pain. Eric and I were laying in the next run of cross braces between the joists. We've been putting them every 4 feet, so as to line up with the joints between T&G plywood sheets. It may be a bit more than code requires, but everything about this floor so far has been.
 
We had measured everything the day before, so I took to cutting the wood. I have to say that after several sessions using the handheld circular saw, I am getting pretty good for a blind guy. The laser sight helps a lot, as does overcoming the general fear of the thing. Also, you have to set the blad depth just right to avoid the extra resistence that comes from cutting just across the sruface of the board below, and you need to keep in mind how the wood will shift, which side of the blade the line is on, and how your own natural arc of arm motion will affect the cut. After trying this kung fu many times, I have gotten to a nice place where my cuts are almost always true and square, and if they are off it's generally only by a 1/16 of an inch or less. Even so, if you have a chop saw or table saw that can do the job instead, I think that'd absolutely be faster.
 
Earlier, we had already penciled out guidelines on the joists where the braces needed to go. I'd marked with a sharpee the top of every joist at exactly 4 feet from the previous floorboards' edge. Unlike before when we were hanging the braces from an open ceiling below, we were now doing this above and the ceiling below us was still "intact", so I couldn't use the carpenters square.
 
Instead, I used a peice of small posterboard with marks every 1/2" to draw square vertical lines offset by 1/2" and 1" respectively from the 4' markings. This gave me 2 lines that were 1 1/2" apart to place the two-by between. In each space between joists, I alternated which way to do the offsets so that the boards would be staggered by only 1/2". This gives them the ability to brace on another, but also provides enough room to end-screw into the boards without much in the way of toenailing. A slight angle to allow for the drill is all that is really needed.
 
Bringing our wood upstairs, we set everything up: plenty of screws, a hammer and a peice of bashing wood (so as not to mark up the braces themselves), and my drill. I would test fit each board and was surprised that most fit very well this time. (My earlier bracing was much less accurate.) I had one that needed to be trimmed down by a quarter inch, which turned out to be caused by the joist pitching to one side which was throwing my measurement off by a lot. If any contractor ever tell syou that you don't need braces in a floor, or that you don't need them soon after installing the joists, fire him immediately. These joists did not take long to pitch out, especailly after being secured to the existing rafters.
 
Some of the braces gave me trouble because of the pitch, and I was able to partly straighten this out. Since i knew my cuts were true, I lined up my brace to the pitching side and screwed it in with three screws, then I applied some pounding and someweight to bring the other side down. The screws held the joist and un-pitched it to a certain extent. The hard part was keeping weight and leverage on the other side while trying to screw it in - a skinny 11 year old is really not heavy enough for this.
 
I left the last brace out so we'll have enough room to bring up two full 4x8 peices of plywood. I really didn't get naything meaningful done after that. By then, my injuury from the day before had caught up with me. Later I plan to put in some insulation under the subfloor, and have some of the wiring done in the bathroom ceiling before I close it up.
 
Update:
As it turned out, I broke one of my ribs in the fall, but didn't discover it until the following Friday. I was sitting in my cube minding my own business when I sneezed. The muscle spazm must've finished off what the fall started, because the pain split my midsection like an axe.
 
I've never actually felt so much pain that I felt dizzy and nausious, but this time I sure did. I nearly passed out, so a visit to the doctor was definitely in ordr even if there was little they could do. An x-ray confirmed that I'd definitly cracked one rib - at least part-way through.
 
Surprisingly, only a few days later I'm on the mend - and not only was I able to stop using the Vikadin (sp?), but I also cut back the Ibuprofen to almost nothing.
Published: Oct-15-07 | 0  Comment | 0  Link to this post

Oct10

Workflow Woes

I have been working on a workflow solution for a client. Having looked over their existing solution, my first impression was that we could replicate the same functionality using only some custom lists and maybe a field control or two. Then I got some additional requirements, and now it is more clear that they need some InfoPath forms around teh item creation workflow to make sure different people all provide their little slice of information.
 
So, I have been folllowing Nick Swan's walkthrough on creating a custom workflow in VS.net. At first I was following along great. Everything seems very much as if it were done in BizTalk, which I know pretty well, but I started running into issues during deployment.
 
My first mistake was a no brainer, but it did trip me up for a minute. In teh FEATURE.XML file, there is an attribute called Hidden that does not get included in the code snippit. If you want your feature to show up in the web admin, you'll need to make htis FALSE. Of course the vs.net post-deployment task will actually activate the feature for you too - I think - but this did not stop me from scratching my head for a few minutes anyway.
 
The next issue is that the sample workflow will only work with Scope=Site (that's as in "SPSite" not "web site" by the way). I don't know why this is. Maybe the standard Microsoft provided feature receiver can't handle workflows that are designed this way in the web context? In any case, changing the FEATURE.XML back to Scope=Site got me through this problem.
 
What will happen if you try to activate the feature at the Web (web site) level is that you will see in the LOGS folder that FeatureReceiver.FeatureActivated is called and throws an NullReferenceException (e.g. "object reference not set" exception). Here are the details from the log file:
 

Calling 'FeatureActivated' method of SPFeatureReceiver for Feature 'SeqWorkflowLab1' (ID: 'cff78dbc-0e13-40f1-a648-91a44c7e7da1').               

10/10/2007 10:32:28.98 w3wp.exe (0x090C)                       0x0354  Windows SharePoint Services   Feature Infrastructure                        88jm      High       Feature receiver assembly 'Microsoft.Office.Workflow.Feature, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c', class 'Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver', method 'FeatureActivated' for feature 'cff78dbc-0e13-40f1-a648-91a44c7e7da1' threw an exception: System.NullReferenceException: Object reference not set to an instance of an object.     at Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver.FeatureActivated(SPFeatureReceiverProperties properties)     at Microsoft.SharePoint.SPFeature.DoActivationCallout(Boolean fActivate, Boolean fForce)       

 

Feature Activation: Threw an exception, attempting to roll back.  Feature 'SeqWorkflowLab1' (ID: 'cff78dbc-0e13-40f1-a648-91a44c7e7da1').  Exception: System.NullReferenceException: Object reference not set to an instance of an object.     at Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver.FeatureActivated(SPFeatureReceiverProperties properties)     at Microsoft.SharePoint.SPFeature.DoActivationCallout(Boolean fActivate, Boolean fForce)     at Microsoft.SharePoint.SPFeature.Activate(SPSite siteParent, SPWeb webParent, SPFeaturePropertyCollection props, Boolean fForce)               

 

Exception Type: System.NullReferenceException  Exception Message: Object reference not set to an instance of an object.                  
 
Ok. Got through that. Next issue.
 
Silly me, I never upgraded my SharePoint to Enterprise edition. So, I picked through my license keys from MSDN and found the code to activate the upgrade. Then I had to activate the enterprise features in the existing sites, and do an iisreset. No biggie, but I should've done this a while ago.
 
So now my workflow is fully activated and it appears in the list of workflows when I go to List Settings > Workflow Settings. I enter all the appropriate info, and check [or don't] the "Start this workflow when a new item is created." checkbox.
 
A box comes back that says, "The specified form cannot be found." What gives? So, I go into Central Adminsitration > Application Management under the section called InfoPath Forms Services there is a link called "manage form templates". In there I do see my two new forms, which is good. I don't know if they've been activated to work with a specific site collection, and I am suspecting they haven't been, but as it turned out you can't do this because these are workflow forms. So, moving on, why can't the list "see" my form?
 
Well, I did a little reading and I found that many people are having problems with this error. The primary cause is that their FEATURE.XML and WORKFLOW.XML are not set up correctly and therefore they are not actually deploying the needed XSN files to the server. But this is not my issue.
 
Eureka! I've found it. Watch out for this when you create and publish your InfoPath forms. It seems that the URN (unique ID) is changed each time you publish the form.
 
Compare this from the XSN in my IP project folder:
urn:schemas-microsoft-com:office:infopath:WorkflowLab1-InfoPath1:-myXSD-2007-09-28T18-41-02
With this from my XSN that I places in the Features folder:
urn:schemas-microsoft-com:office:infopath:Initiation-Form:-myXSD-2007-09-28T18-41-02
Also, notice the timestamp! It's possible this changes every time you publish a new version, which would mean updating your XML every time you make a change to the form.
 
Update:
 
Okay, so I finally get everything installed properly and now I am able to associate my workflow with a list. The association form is ugly as hell and doesn't make any sense because it is the same as the initiation form, but that's ok for now.
 
However, I am getting a SerializationException when I attempt to create a new object, and the workflow terminates. At least now it is in my code, and I know what is going on, because my workflow is trying to deserialize the XML that comes from the InfoPath initiation form.
 
Digging into the autogenerated class produced by XSD gives me a good explanation for why it would not work. There are anyAttribute properties peppered all over it. This is doubly complicated by the fact that I used a rich edit control in my IP form.
 
So, rather than try to kit bash the sample into doing what I want or willy-nilly commenting out parts of the class file every time I change the XSD, I've decided to go back to a best practice that I used to follow in IP/SharePoint 2003. I will be creating my XSD file first - by hand, and then beinding my IP form to it after it has been defined and the class files have been generated. Also, I think I will use XsdObjectGen instead of xsd.exe because I think it generates code that is easier for a developer to use. (Actually it won't be by hand. I like to use the BizTalk XSD editor to do this.) In my mind this is the essence of the contract first principle of SOA.
 
Update II:
 
Laziness may win in the end, because I really would like to see this work.
 
Upon further reflection and looking at the stack trace, I decided that maybe the serialization error isn't happening exactly where I think it is. Is it possible that the InfoPath schema deserializes correctly and my mistake is then in assigning its properties to my workflow instance's ExtendedProperties collection?
 
WinWF Internal Error, terminating workflow Id# cb38d1cb-e216-488a-9cc3-f9a1df33129d 
10/10/2007 12:32:24.51  w3wp.exe (0x126C)                        0x0558 Windows SharePoint Services    Workflow Infrastructure        98d4 Unexpected System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.InvalidOperationException: The platform does not know how to deserialize an object of type Instructions. The platform can deserialize primitive types such as strings, integers, and GUIDs; other SPPersistedObjects or SPAutoserializingObjects; or collections of any of the above. Consider redesigning your objects to store values in one of these supported formats, or contact your software vendor for support.     at Microsoft.SharePoint.Administration.SPAutoSerializingObject.DeserializeBasicObject(XmlElement xmlValue)     at Microsoft.SharePoint.Administration.SPAutoSerializingObject.DeserializeBasicObject(XmlElement xmlValue)     at Microsoft.SharePoint.Administration.SPAutoSerial... 
10/10/2007 12:32:24.51* w3wp.exe (0x126C)                        0x0558 Windows SharePoint Services    Workflow Infrastructure        98d4 Unexpected ...izingObject.SetStateXml(XmlNodeList childNodes)     at Microsoft.SharePoint.Administration.SPAutoSerializingObject.SetStateXml(XmlDocument value)     at Microsoft.SharePoint.Administration.SPAutoSerializingObject..ctor(SerializationInfo info, StreamingContext context)     at Microsoft.SharePoint.Workflow.SPWorkflowTaskProperties..ctor(SerializationInfo info, StreamingContext context)     --- End of inner exception stack trace ---     at System.RuntimeMethodHandle._SerializationInvoke(Object target, SignatureStruct& declaringTypeSig, SerializationInfo info, StreamingContext context)     at System.RuntimeMethodHandle.SerializationInvoke(Object target, SignatureStruct declaringTypeSig, SerializationInfo info, StreamingContext context)     at System.Reflection.RuntimeConstructorInfo.Serializat... 
10/10/2007 12:32:24.51* w3wp.exe (0x126C)                        0x0558 Windows SharePoint Services    Workflow Infrastructure        98d4 Unexpected ...ionInvoke(Object target, SerializationInfo info, StreamingContext context)     at System.Runtime.Serialization.ObjectManager.CompleteISerializableObject(Object obj, SerializationInfo info, StreamingContext context)     at System.Runtime.Serialization.ObjectManager.FixupSpecialObject(ObjectHolder holder)     at System.Runtime.Serialization.ObjectManager.DoFixups()     at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)     at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)     at System... 
10/10/2007 12:32:24.51* w3wp.exe (0x126C)                        0x0558 Windows SharePoint Services    Workflow Infrastructure        98d4 Unexpected ....Workflow.Activities.InvokeHelper.CloneOutboundValue(Object source, BinaryFormatter formatter, String name)     at System.Workflow.Activities.InvokeHelper.GetParameters(MethodBase methodBase, WorkflowParameterBindingCollection parameterBindings, ParameterModifier[]& parameterModifiers)     at System.Workflow.Activities.CallExternalMethodActivity.Execute(ActivityExecutionContext executionContext)     at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(T activity, ActivityExecutionContext executionContext)     at System.Workflow.ComponentModel.ActivityExecutor`1.Execute(Activity activity, ActivityExecutionContext executionContext)     at System.Workflow.ComponentModel.ActivityExecutorOperation.Run(IWorkflowCoreRuntime workflowCoreRuntime)     at System.Workflow.Runtime.Scheduler.Run... 
10/10/2007 12:32:24.51* w3wp.exe (0x126C)                        0x0558 Windows SharePoint Services    Workflow Infrastructure        98d4 Unexpected ...() 
Published: Oct-10-07 | 0  Comment | 0  Link to this post

Oct05

Custom Fields and Content Types

In a word: BE CAREFUL.
 
Okay, that is actually two words. :-)
 
Just be careful about the naming conventions that you use and how you change them later. If you need to uninstall / reinstall a custom field and control, you will likely have problems with the lists that were built on top of it. You will also have problems with the SharePoint Site Column and Content Type management pages. Needless to say, this is NOT good.
 
Fortunately, it looks like the fields can be manually deleted using the SharePoint web services. I imagine it is also possible to do this from stsadm, but I never much liked command line tools, so I am trying now to create a little Windows Forms application that will do the job for me.
 
I will post my code back here when I get it stable. For now, just be careful about changing or removing custom field names and namespaces, etc.
 
Update:
 
Okay, there is only a very small amount of useful stuff online at this point, so I will try to be as specific as I can - for a Friday evening.
 
My Issue and Its Root Cause
 
Create a custom field control and field type and install them to SharePoint either by hand, with an MSI, or a WSP. In particular, I want to focus on the FLDTYPES_xyz.xml file (or whatever you decide to call yours). This is the file that tells SharePoint how to make use of your custom field control.
 
Now, suppose you decide later that you don't like the name of one of your fields. Maybe you mis-spelled it, or maybe (like me) you just decided that you didn't want there to be any naming ambiguity between your custom fields and those that other developers might provide. So, you rename your field in the definition. You think it will be okay because you are going to uninstall and reinstall your solution anyway.
 
Here is the gotcha. If you have used your custom field in any list, then a reference to it will have been added to the SPWebs.Fields collection, which is a master list of all the field definitions that are used for your entire web site. Additionally, if you have added it to any of your content types, then a reference will exist in the form of a SPFieldLink in the SPWeb.ContentTypes[x].FieldLinks collection.
 
If you remove the FLDTYPES.xml file that these references are relying upon, or change the names in it, then you'll see some pretty interesting behavior out of SharePoint! Hopefully what I am posting here will save somebody the entire workday that this issue cost me.
 
What manifests is that your lists, the content types admin web page, and the site columns admin web page will all be broken by this change. In particular, the site columns page will give an error:
The given key was not present in the dictionary.
Very helpful indeed - as helpful approaches zero. The other pages will have other cryptic messages as well, but I did not capture them here. Maybe some time I will go back and repro this error so I can take some screen shots. Sadly, nothing I did to the CustomErrors settings on my server allowed me to see a stack trace.
 
What you can do to fix this behavior is change the TypeName property back to what it was in your XML file - assuming you can remember what it was.
 
<Field Name="TypeName">MyOldTypeName</Field>
 
If you can't remember the name you used that is causing you the problem, you can use the SharePoint API to find it.
 
Simply open your web site with an SPWeb object, then attempt to iterate through the SPWeb.Fields collection. You will immediately receive an exception to the effect of something like "FieldType named 'x' is not properly installed. Go to the Site Collection page in Site Settings to correct the problem." Of course this is bollocks, because the web page is broken by this problem.
 
   string customColumnName = "My_x0020_Custom_x0020_Column";
   string contentTypeName = "MyContentType";
   using (SPSite site = new SPSite( "
http://spdev" )) {
    using (SPWeb web = site.OpenWeb( "/site1" )) {
     // just some useful code for getting the lsit of names
     List<string> contentTypeNames = new List<string>();
     SPContentType ct = web.ContentTypes[contentTypeName];
     foreach (SPFieldLink link in ct.FieldLinks) {
      contentTypeNames.Add( link.Name );
     }
     List<string> fieldNames = new List<string>();
     foreach (SPField field in web.Fields) { // <!-- this is where exception gets thrown!
      fieldNames.Add( field.Title );
     }
     // delete the column from all content types
     ct.FieldLinks.Delete( customColumnName );
     ct.Update( true );
     // delete the site column / field
     web.Fields.Delete( customColumnName );
     web.Update();
    } // using SPWeb
   } // using SPSIte
 
In fact, as it turns out, my first approach was totally wrong, because I was trying to use the web service or API to simply remove the offending site column since I didn't want to keep it anyway. I was able to get rid of the reference to the column in the content type, but SPWeb.Fields.Delete("FieldName") throws the same error I describe above, so you can't actaully delete the column as long as it is still in a broken state. Nice!
 
But, now you will have the name of the field type name that can't be found - and hopefully some ability to stitch togehter a FLDTYPES.xml field to match it, either using your own control or one of the base field controls from SharePoint. Having corrected the issue you should be able to use the pages on Site Settings to delete the offending column and/or remove it from any content types.
 
However, bear in mind that if it is in use in any lsits, then it will be removed and any data housed there will be lost. If you need to keep you data you will either have to find a way to move it into another field or else leave the naming conventions the way they were.
 
What I Learned About SharePoint from This Mess
About the most useful thing I learned today is that it's about 1,000 times easier to make programmatic changes to columns and content types if you use the APU than it is using the web services. Sure, the web services are there and they have their uses I suppose. But, you'll have to learn to write some CAML and get used to parsing your results using XPath queries, because the results don't deserialize (at least not easily).
 
I wrote a nifty applet that can connect to a web site using the Webs.asmx server and let you browse site columns and even select one for deletion. Getting the UI populated was a chore, because the XML that the web services return needed to be massaged to strip the namespaces from it so that SelectNodes would actually work. (It was either that or implemenet a NamespaceManager, which let's just say wasn't going well.) In the end, though I got all the code written to pick a column from a column group and then delete it, it didn't work anyway! This was  because of the issue that caused the exception I describe above, I guess. But, crossing the web service the only error information you will see is "800040005 Operation Failed". Sadly, that's not very useful.
 
So, in the end using the web service to do this type of administrative work is probably not worth it. The only real benefit you'd get from a lot of extra effort is that your admin tool would be able to connect to services across the internet even if they haven't installed anything on the server side. If you have control over the server environemnt it would probably be much better to write your own easy to use web service wrapped around the API functions you want to implement.
 
Another thing that I learned is that the SharePoint web services still largely use CAML passed as either strings or untyped XmlNode objects. These are somewhat difficult to work with, and I will have to do some research into finding out how they can either be serialized/deserialized into useful objects in >NET or otherwise made easier to handle. On the birght side, I wrote a useful serialization tool for SPQuery CAML code some time ago and it looks liekt hat will probably still be relevant and I should go dust it off. :-)
 
Finally, I leanred a lot about the naming conventions for columns and content types as they realte to the API, the web service, and the terminology on the web site itself. This can be pretty confusing, and there's not a lot of good documentation out there. But, it's a little out of scope for today and I'll just have to put together another article for that topic some other time.
 
Now, it's off to New York to see my in laws and hopefully spend a nice weekend with my head buried in my laptop and some comic books.
 
More Information:
Published: Oct-05-07 | 3  Comments | 0  Link to this post

`