Blog Moved

Move along, there’s nothing to see here.

I’ve given up on Community Server as my blog engine and have moved to WordPress.  The new blog is located here: http://blog.randomdust.com.  I’ve moved over all current posts to the new site, so if you’ve wanted to comment on anything but weren’t able to on this blog, move on over to the new site and comment away.

At some point I may add some redirects to the new location, but not today.

Read Multiple Values from Person or Group Field

When upgrading from SharePoint 2007 to 2010, one of the areas you need to look at is any custom code that was written for the 2007 environment.  Depending on what that code is doing, the effort to upgrade it may be larger than initially thought.  I recently ran into such a situation, and discovered some oddities I hadn’t run into before regarding Person or Group fields and how the values from those fields are submitted from a datasheet view.

First, the basics of the relevant code for the issue.  There is an event receiver that handles the ItemUpdating event and it needs to read the values a user enters into a People or Group column that allows multiple selections.

The 2007 code used to read the new values entered was:

   1: object peopleAfterProp = properties.AfterProperties[listItem.Fields[“FieldName”].InternalName];
   2: SPFieldUserValueCollection newUsers = new SPFieldUserValueCollection(listItem.Web, peopleAfterProp.ToString());

This code read the new values entered by the user and created a new SPFieldUserValueCollection object containing an SPFieldUserValue for each person or group entered.  It worked fine in 2007 for both standard list views and datasheet view.  Didn’t work quite as well in 2010. :)

In 2010 for standard list views, properties.AfterProperties for the field returns values such as "9;#BLACKDOG\\normaluser1;#2;#BLACKDOG\\ryan", which is perfect and creates a valid SPFieldUserValueCollection.  In 2010 datasheet view, the same field and the same users returns a value of "9;#;#2;#".  When the SPFIeldUserValueCollection is created using the code shown above, only a single SPFieldUserValue is created for the first user (ID = 9), and it’s invalid.  So basically, the datasheet view in 2010 for People or Group columns has what I would call a bug (MS might disagree) and only returns data containing the user IDs from AfterProperties.  It’s thus impossible to create a valid SPFieldUserValueCollection object.

There’s the problem, now how to fix it.  Since datasheet only returns the IDs, and the string format (using the “;#” delimiter) is the same as when submitted from a list view, we can key off the IDs.  If we can extract the IDs of the users, we can then quite easily go on to create SPUser objects, which is really where we need to get.  The goal is to find some code that we can use to create user objects regardless of which view the user is submitting data through.

My thought was to get a Dictionary object populated with key value pairs when a standard list view is used, and the same Dictionary object populated with just keys when datasheet view is used.  To parse out the values, I started by using some code posted by Tony DalPezzo, specifically his GetDelimitedLookupValuesDictionary method.  After some work with the regular expression (which is not my strong suit, by the way!), I ended up with the method:

   1: public static Dictionary<int, string> GetDelimitedLookupValuesDictionary(string sFieldValue)
   2: {
   3:     Dictionary<int, string> results = new Dictionary<int, string>();
   4:  
   5:     System.Text.RegularExpressions.Regex regexMultiValueField = new System.Text.RegularExpressions.Regex(@"(?<id>(-?\d+));#(?<value>(\w+\\+\w+)?)(;#|$)", 
   6:         System.Text.RegularExpressions.RegexOptions.Compiled);
   7:     System.Text.RegularExpressions.Match choice = regexMultiValueField.Match(sFieldValue);
   8:  
   9:     while (choice.Success)
  10:     {
  11:         results.Add(Convert.ToInt32(choice.Groups["id"].Value), choice.Groups["value"].Value);
  12:         choice = choice.NextMatch();
  13:     }
  14:  
  15:     return results;
  16: }

Feel free to critique my regex, but it works.  Regardless of which view the user submits data through, I get a valid Dictionary object back.  Now we can create List<> objects containing our SPUsers by looking up each user in the Dictionary.

   1: Dictionary<int, string> peopleAfterDic = GetDelimitedLookupValuesDictionary(peopleAfterProp.ToString());
   2: List<int> peopleIdsAfter = new List<int>();
   3: List<SPUser> peopleAfter = new List<SPUser>();
   4:  
   5: foreach (KeyValuePair<int, string> personAfter in peopleAfterDic)
   6: {
   7:     // Ensure the user exists in the site collection (only possible to select users that aren't through list view, not through datasheet view)
   8:     if (personAfter.Key == -1)
   9:     {
  10:         SPUser tempUser = listItem.Web.EnsureUser(personAfter.Value);
  11:  
  12:         peopleIdsAfter.Add(tempUser.ID);
  13:         peopleAfter.Add(tempUser);
  14:     }
  15:     else
  16:     {
  17:         peopleIdsAfter.Add(personAfter.Key);
  18:         peopleAfter.Add(listItem.Web.SiteUsers.GetByID(personAfter.Key));
  19:     }
  20: }

Problem solved.

Hopefully this will be fixed in an update from Microsoft.  If / when it is, this code will still work, but going back to the expected SPFieldUserValueCollection method would probably be best.

P.S. Notice the check in the code above for a Key value of –1.  That means the SPUser that the end user chose through the standard list view isn’t yet a user listed in the site collection.  So we need to call EnsureUser, which adds the user and returns the valid SPUser object.  This can’t happen with the datasheet view as the UI only allows selecting existing site collection users.

Application Templates for SharePoint 2010??

For those folks who skip to the end of the book to see how things finish, I’ll jump to the point.  Doesn’t look like Microsoft is releasing any of the Application Templates (i.e. Fabulous 40) for SharePoint 2010.  So says To the SharePoint blog, and mentioned on the home page for the existing 2007 application templates.

That’s the bad news.  Now, if you’re still with me after that lengthy paragraph, here’s the good news.  The community has been stepping up to fill the gap.  Specifically, a company called TechSol.  So, if you absolutely have to have one of the Fab 40 templates, perhaps it’s been updated for you.  See the links below for the great work they’ve done.  As always, please use caution and test before installing in a production environment. :)

Fab 40 for SharePoint 2010 Foundation

Fab 40 for SharePoint 2010 Server

Could not establish trust relationship for the SSL/TLS secure channel

On a recent project, we had a requirement to use a SharePoint Designer workflow to call an external web service.  Instead of writing my own custom Activity, I did a search to see what was out there and found the iLove SharePoint Designer Actions.  One of the actions he has in there is one that can call a SOAP 1.1 or 1.2 web service, which is exactly what I needed.  Although it was written for SharePoint 2007, I pulled it into SharePoint 2010 and tested it out and found it to work just fine.

The end point for our test web service was an http address.  The end point for production was https.  As soon as we switched to the production end point, we began getting the error:

System.Net.WebException: The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.

That’s is the error we saw in the ULS logs.  The nice error we saw in the workflow history was something useless like “An exception occurred”.

The error boils down to the code not trusting the secure certificate.  I tried a couple non-code solutions first, one modifying the web.config:

   1: <servicePointManager checkCertificateName="false" checkCertificateRevocationList="false"/>

And another importing the security certificate on the server.  Neither option worked for me, although some postings I found on the Internet suggested one or the other worked for some folks.

So after exhausting the non-code solutions, I dove into the code of the iLove SharePoint Actions to add some code to tell it to ignore the certificate check.  Some searching pointed me to here and here.  So I pulled down the iLove SharePoint source from codeplex, opened it up in VS2010 and upgraded it.  I had to change the Microsoft.SharePoint.dll references since I was using SharePoint 2010, and there were some build commands that needed to be updated as well.  Once those were done, I added the line:

   1: ServicePointManager.ServerCertificateValidationCallback = (obj,certificate,chain,errors) => true;

Right before the call to the web service.  Once that was built and deployed, problem solved.

Looking at it from a security perspective, probably not the best solution as it opens up the possibility to call services on sites with truly invalid certificates, but being in a closed environment it was acceptable.

Selling and Delivering SharePoint Governance

Look up the word “governance” in wikipedia and you’ll find sentences such as “It relates to decisions that define expectations, grant power, or verify performance. It consists either of a separate process or of a specific part of management or leadership processes. Sometimes people set up a government to administer these processes and systems.”  Ok, that’s not terribly vague.  Now, trying to put that into SharePoint terms the following come to mind:

  • Authentication
  • Authorization
  • Business Continuity
  • Customization Policy
  • Deployment Model
  • Governance Model and Policy
  • Governance Board or Committee
  • Testing
  • Training

And on and on.  If you take some time browsing through the SharePoint Governance site on TechNet, you’ll find a wide range of topics as well as multiple sample governance plans in the content and links therein.  Point being, the single word of “governance” quickly expands to cover a wide range of topics.  If we’re not too careful when selling or delivering governance, we can easily get caught up in trying to cover every angle and aspect and can end up creating a true Government bureaucracy that rivals Washington D.C.  Not a good thing for SharePoint to be successful.

Because SharePoint Governance covers such a wide range of topics and companies have varying priorities and strengths/weaknesses, governance is defined differently for any given company.  There is no “one size fits all”.  This makes it quite difficult, if not impossible, to provide an accurate list of topics that must be defined in order for the governance check box to be marked as complete.  Another complexity is that if governance is done right, it never is complete.  It’s living and grows with the organization.  Whether you are in consulting or are a driving force for SharePoint within a company, this makes selling the concept of governance difficult.  Maybe to clarify a bit, it’s difficult to sell governance to leadership and have a clear definition of what will be delivered.

To give an example, I recently completed a governance project (the inspiration for this post) where the definition of what was to be delivered was defined up front down to the governance topic level.  That was great for both estimating level of effort and defining expectations with management up front, but due to the variations mentioned above, that plan needed to be flexible.  It wasn’t.  It was near impossible for a PM to track actuals back to specific tasks listed in the governance definition because as the process of discovering the governance needs of the organization progressed, those tasks needed to change. 

How to be successful then?  First and foremost, get executive commitment.  This is one SharePoint topic where I think it must come from the top to be successful.  If the executives show support and dedication, and back up that statement with funds and a project to ensure its success, the entire company will see that commitment and organization and be driven to meet expectations.  Second, define scope by listing areas of focus instead of specific policies to include in the governance plan.  For example, operations, user training and experience, and the definition of the governance process are all items that must be touched on and the detailed definition of which comes through the process of defining governance.  List them in the scope, and deliver by holding stakeholder meetings and make recommendations based on the needs and capabilities of the organization.

Finally, there is the discussion of time and cost.  As a rule of thumb, I’d recommend three to six weeks.  At least three to get to the point where governance has been defined enough to be able to kick off a governance process, and up to six to make that process more refined and solid to eliminate some of the vague areas that will exist if only a three week effort.  Following the initial effort is where the executive commitment really pays off as the process involves the right people at the right frequency to ensure the governance process and policies grow and change to meet the organizational needs.

Beard-A-Thon

I'm reaching out to you (if anyone still reads this blog) looking for some support with a charity. For those of you that are hockey fans, you're familiar with the NHL tradition of the players growing beards as long as their team is in the playoffs. If you're not hockey fans, trust me, they do. There's a new charity (at least new to me) that is extending that to fans and at the same time working to raise money for charities. I decided to participate and see if I can become The Most Interesting Man in the World!

The Colorado Avalanche charity benefiting from this program is KSE Charities, a charity that supports Colorado kids and families by providing health, education, literacy and fitness programs. A good cause, if you ask me (otherwise, I wouldn't be doing it!) Any donation you make will be tax deductible.

If you're interested in donating, or just interested in following along in the path to a great beard, check out http://www.beardathon.com/avalanche/rmcintyre/profile.aspx.

Thanks for taking the time.

Ryan

Document Version Properties

I'm currently working on a SharePoint project involving a requirement to embed the current version into each document.  Not an uncommon requirement, but at the end of the day it required an uncommon solution due to various issues I hope to post about.  Along the way I found myself needing to access the AfterProperties in an event receiver in order to read the current version of the document.  I knew the version info was in there somewhere, but I didn't know which exact property I needed.  To figure out my possibilities, I added the below code to my event and ran through it with the debugger seeing what was there:

//Loop through all properties to see what's available

foreach (DictionaryEntry prop1 in properties.AfterProperties)

{

    string key = prop1.Key.ToString();

    string value = prop1.Value.ToString();

}

 

The two properties that looked good were vti_docstoreversion and vti_sourcecontrolversion.

 

After some investigating using the debugger and some different test cases I determined that vti_sourcecontrolversion was the one I needed.  

 

The vti_docstoreversion stores an integer that represents how many times the document has changed.  The vti_sourcecontrolversion stores the version you see in the UI, such as "2.1".   As an example, with major and minor versioning enabled for a library, add a document.  Without publishing, it's displayed version will be .1.  Make an edit and it will be .2.  Publish a major version and the displayed version will be 1.0.  In this example, the docstoreversion value will be 3 and the sourcecontrolversion value will be 1.0.

 

I hope that helps someone as I didn't find this documented anywhere online.

Posted 24 June 09 08:09 by ryanm1201 | 0 Comments   
Filed under , ,
Changing the App Pool Identity using STSADM

Ok, ok...I didn't write it.  But I was in the room when it was written!! 

My co-worker Gary Lapointe presented last week to the Colorado Springs SharePoint User Group on customizing STSADM and his "demo" was actually writing a new, functional STSADM command.  Very simple to do.  Visit his site for the details.

Not only did I learn some more about Gary's tools, I won the raffle for an MSDN Premium license (with Team Suite!)  Well worth the trip.  Thanks Gary!

MCS Co-Worker Finally Blogging
It took him a while, but Sameer Surve finally has a blog!  He's been my MCS counterpart at my current client for the last year and a half (give or take), and he definitely has lots of SharePoint and BizTalk info worth sharing.  I guarantee you'll have at least one subscriber, Sameer!
Web.Config FeatureReceiver Update

After my previous post outlining my custom Feature Receiver used to update any section of a web.config file, I had some follow up email conversations with Mike about his post and us possibly collaborating on a combined CodePlex solution.  Well, that hasn’t happened yet.  However, during that discussion, we talked a bit about security.  Mike referred me to a post by a colleague of his inspired by this very topic.  The gist of the discussion and post is that modifying the web.config with a Feature Receiver really only makes sense, from a security perspective, when the Feature is scoped at the web level.  I agree, so I needed to make a small change to my custom FeatureReceiver.

The change I needed to make was simply to make sure that it did not update the web.config if the Feature was not deployed at the web scope.  This was a pretty simple change, but I took the opportunity to do a bit more research on the topic to see what others had done.  The best nugget I pulled out of this research time was a change in the way I was applying the updates.  Turns out there is an issue applying an update using

SPFarm.Local.Services.GetValue< SPWebService>().ApplyWebConfigModifications();

Using that method, the changes don’t get applied to the entire farm.  Since we’re in a farm environment, I would’ve run into that at some point.  The solution is to use this instead:

webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();

Continue to the end of this post for some more reading on the topic.  For now, here is the code update I made:

   1: private void UpdateWebConfig(string XPathLocation, string ElementName,  
   2:     Dictionary<string, string> Attributes, bool removeModification)
   3: {
   4:     try
   5:     {
   6:         SPWebApplication webApp = null;
   7:  
   8:         //Get the web app
   9:         webApp = _properties.Feature.Parent as SPWebApplication;
  10:  
  11:         if (webApp == null)
  12:             throw new SPException("Error obtaining reference to Web application. A feature must " +
  13:                 "be deployed at the WebApplication level to use the WebConfigChanges feature.");
  14:  
  15:         SPWebConfigModification modification =
  16:             new SPWebConfigModification(ElementName + CreateAttributeString(Attributes), XPathLocation);
  17:             //new SPWebConfigModification("authorizedType[@Assembly=\"Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4\"][@Namespace=\"Company.Moss.Activities\"][@TypeName=\"*\"][@Authorized=\"True\"]", "configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes");
  18:  
  19:         modification.Owner = "Company.Moss.FeatureReceiver.FDFeatureReceiver";
  20:         modification.Sequence = 0;
  21:         modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
  22:         modification.Value =
  23:              string.Format(CultureInfo.InvariantCulture,
  24:              CreateModificationValueString(ElementName, Attributes),
  25:              CreateModificationValueArgs(Attributes));
  26:  
  27:         if (removeModification)
  28:             webApp.WebConfigModifications.Remove(modification);
  29:         else
  30:             webApp.WebConfigModifications.Add(modification);
  31:  
  32:         //Changing from SPFarm to webApp as advised by http://www.crsw.com/mark/Lists/Posts/Post.aspx?ID=32
  33:         //SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
  34:         webApp.Farm.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
  35:         webApp.Update();
  36:  
  37:     }
  38:     catch (Exception ex)
  39:     {
  40:         System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
  41:         el.Source = "WebConfigFeature";
  42:         el.WriteEntry(ex.Message, System.Diagnostics.EventLogEntryType.Error);
  43:     }
  44: }

Like I said, this is going to look a lot like the code in my previous post.  The first change is in the first few lines of the try block.  I assume the parent of the Feature is a Web App.  If it gets a reference, great.  If it doesn’t, it logs an error and doesn’t do anything else.  It would be nice if there were a way to actually stop the Feature from being deployed, but that can’t be done in a Feature Receiver (it happens after the Feature is activated.)

The second change is at the end of that code snippet, and is to account for the Farm update issue I mentioned earlier.

That’s it.  Like I said, not much has changed, but I wanted to put out an update in case others are using this approach.

For further reading, here are some references:

Reza Alirezaei – SPWebModification’s Top 6 Issues
Mark Wagner – How To Modify the web.config File in SharePoint Using SPWebConfigModification
Serge van den Oever – SharePoint Features – elements, scope and other info

(That last one is useful because he lists the types of features that are allowed at each scope.  Can save you some time so you don’t implement this method to deploy something that can’t be deployed at the WebApplication level.)

Posted 21 July 08 10:09 by ryanm1201 | 3 Comments   
Filed under , ,
InfoPath Master/Detail with SharePoint List Sources

I picked up some InfoPath requirements after one of our consultants had to leave so this one had to be done after dusting off the InfoPath brain cells.  I’m still not convinced this is the best way to get this done so please leave a comment if there’s a better way.

The InfoPath form needs to connect to two different SharePoint lists.  The first list is a list of Orders and the second is a list of Versions that apply to an Order.  There can be multiple Versions for each Order.  The goal was to create a master/detail type action where a user selects an order from a drop down, which then reloads the form with all of the version(s) and version details for that order displayed in a repeating table.  Doesn’t sound too hard, but I ran into some road blocks and had to resort to code.

Both roadblocks had to do with SharePoint lists as a source.  I don’t know if this is specific to SharePoint sources or just external sources in general, but you can’t use a SharePoint source in a master/detail relationship.  After dragging it onto the form, right click on the repeating table and view the properties.  The master/detail section is grayed out.  The second thing I ran into was trying to filter the SharePoint connection.  No luck there either, it’s not available.

After some searching, I found a post on implementing master/detail for a web based form which detailed some steps to pull in SharePoint data using a different method.  Instead of using the SharePoint List option when creating the connection, you use the XML option and point it to a URL which outputs the list data in XML format.  I won’t cover the details of that here as he covers it his post very well, but it’s using owssvr.dll.  That post solved my issues completely, but it was using code for InfoPath 2007.  I needed to convert that to be 2003 compatible. 

Since the 2003 compatible code is probably the only useful part of this post, here it is:

   1: public void OrderName_OnAfterChange(DataDOMEvent e)
   2: {
   3:     //Capture the new value for Order so we can filter the data just for that order
   4:     string newOrder = e.NewValue.ToString();
   5:  
   6:     //Get a reference to the Card Version Adapter
   7:     XMLFileAdapterObject cardVersion = thisXDocument.DataAdapters["Card Version owssvr"] as XMLFileAdapterObject;
   8:  
   9:     //Modify the Card Version adapter URL to append the filter
  10:     cardVersion.FileURL = cardVersion.FileURL + "&FilterField1=Order_x0020_Name&FilterValue1=" + newOrder;
  11:     
  12:     //Re-query
  13:     cardVersion.Query();
  14:  
  15:     if (e.IsUndoRedo)
  16:     {
  17:         // An undo or redo operation has occurred and the DOM is read-only.
  18:         return;
  19:     }
  20:  
  21:     // A field change has occurred and the DOM is writable.
  22:     // Write your code here.
  23: }

Pretty simple, but it took some SDK searching to find the right classes.  I’m using the XMLFileAdapterObject to grab the “Card Version owssvr” data connection, then I change the URL using FileURL similar to how Ishai modified the FileLocation property of the FileQueryConnection object.  Like I said, nothing fancy, but I thought it may come in handy for folks looking for a solution similar to Ishai’s, but who can’t use InfoPath 2007.

Test Twitter Notifier from Live Writer

Trying out the Twitter Notifier using Live Writer.  Found in the Live Writer SDK for Technical Preview.  This should show up on my Twitter feed.

FeedReader Update

Tim just put together a new release for his FeedReader web part which includes the code contributions I made a few months ago.  He details some installation steps on his blog here.

Thanks again for the opportunity to contribute, Tim!

Posted 30 June 08 10:21 by ryanm1201 | 4 Comments   
Filed under
Drill Here, Drill Now!

I guess if you sit and wait long enough, someone will do something to solve a problem.  In my eyes, the problem is our dependency on foreign oil.  There are two angles of attack to solve the problem.  First, reduce our dependency on oil regardless of source through alternative fuels.  This is a great idea and it's awesome to see work done in this area, including my uncle creating a home-brew hydrogen cell as an example.  Second, increase our production of oil so we don't need to depend on foreign sources.  If both of these could be done at the same time I think we'd be well on our way to solving the problem for good.

Not much is needed to increase our production.  The oil is there, we just need to go get it.  To that end, I came across a petition earlier which was started by Newt Gingrich called "Drill Here, Drill Now, Pay Less" which is meant to collect signatures which will be presented to the Senate.  He's already delivered 350,000, and the site is up to 805,000.  One of those signatures belonging to Chuck Norris!  Take a look at americansolutions.com and sign the petition if it looks like a good solution to you!

Posted 16 June 08 07:36 by ryanm1201 | 0 Comments   
Filed under
FeatureReceiver For Applying Custom Web.Config Changes

As I was reading through my SharePoint feeds I came across a post that struck a cord with me.  The post is by Mike Stringfellow and is about updating sections of the web.config through a SPFeatureReceiver.  It struck a cord because I had developed something very similar last July to make it simpler to make changes to the web.config through deployment.  It was always on my TODO list to blog about it but I obviously never got around to it.  Thanks to the "reminder" from Mike, let's see what I did!

(By the way, the purpose of this is not to steal any of Mike's thunder.  Actually, it looks like his solution is built better than mine, so I'll give credit where credit is due.  My hope is to provide some nuggets to the community with the hope that a public solution can be made available.  Mike, if you read this, perhaps we could put together a CodePlex project?)

I'll point out two things worth mentioning, then I'll let the code speak for itself as once you understand these two concepts the rest falls into place.

First is the use of the SPWebConfigModification class to manage the updates to the web.config file.  The class is flexible...it allows you to create any modification, then using xpath define where the modification lives in web.config.  So with a little string manipulation to create the entry and some xpath work to find the location in the config file, all that's left is to determine if we're performing an Add or Remove and then call the proper method on the SPWebApplication.WebConfigModifications.  You'll see this illustrated in the UpdateWebConfig method in the code below.

Second is the use, and format, of a settings file which needs to be packaged with the Feature and defines the changes to be made to web.config.  This allows the developer to simply include a new settings file in their Feature and the FeatureReceiver pulls that in and reads the changes.  No code for the developer.  As you can see in the code, the file needs to be named WebConfigChanges.xml.  Here's the XSD:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="WebConfigChanges" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
  <xs:element name="WebConfigChanges" msdata:IsDataSet="true" msdata:UseCurrentLocale="true">
    <xs:complexType>
      <xs:choice minOccurs="0" maxOccurs="unbounded">
        <xs:element name="WebConfigChange">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="Attributes" minOccurs="0" maxOccurs="1">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded">
                      <xs:complexType>
                        <xs:attribute name="Name" type="xs:string" />
                        <xs:attribute name="Value" type="xs:string" />
                      </xs:complexType>
                    </xs:element>
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
            <xs:attribute name="XPathLocation" type="xs:string" />
            <xs:attribute name="ElementName" type="xs:string" />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>
 
Here's some explanation on the file:
 
Name Description
WebConfigChanges A collection of WebConfigChange entries to be applied to the web.config file. The top element of the file.
WebConfigChange A single WebConfigChange to be applied to the web.config file.
XPathLocation Gives the XPath location to the element where the new entry should be inserted.
ElementName The name of the element to insert.
Attributes A collection of Attribute entries to be applied to the element being inserted.
Attribute A single Attribute to be applied to the element being inserted.
Attribute:Name The name of the attribute.
Attribute:Value The value of the attribute.
 
And an example which enters a new authorizedType entry for a new custom workflow activity:
 
<?xml version="1.0" encoding="utf-8" ?>
<WebConfigChanges>
  <WebConfigChange XPathLocation="configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes"
                     ElementName="authorizedType">
    <Attributes>
      <Attribute Name="Assembly" Value="Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4" />
      <Attribute Name="Namespace" Value="Company.Moss.Activities" />
      <Attribute Name="TypeName" Value="*" />
      <Attribute Name="Authorized" Value="True" />
    </Attributes>
  </WebConfigChange>
</WebConfigChanges>

Hopefully that all makes sense.  That foundation being set, I'll let the code explain the rest.  If you have any questions or suggestions please leave a comment.

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using System.Xml;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
 
namespace Company.Moss.FeatureReceiver
{
    class CustomFeatureReceiver : SPFeatureReceiver
    {
        protected SPFeatureReceiverProperties _properties;
 
        #region base overrides
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
            //throw new Exception("The method or operation is not implemented.");
        }
 
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "\\WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, false);
            }
        }
 
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            //This is where the WebConfigChanges file should be
            string fileLoc = properties.Definition.RootDirectory + "\\WebConfigChanges.xml";
 
            //Check to see if a WebConfigChanges.xml file exists. If yes, we have work to do
            if (System.IO.File.Exists(fileLoc))
            {
                //Grab the properties
                _properties = properties;
 
                this.ProcessChanges(fileLoc, true);
            }
        }
        #endregion
 
        private void ProcessChanges(string FileLocation, bool removeModification)
        {
            string xPathLocation;
            string elementName;
            Dictionary<string, string> attributes = new Dictionary<string, string>();
 
            using (XmlReader reader = XmlReader.Create(FileLocation))
            {
                //Loop through all of the changes
                while(reader.ReadToFollowing("WebConfigChange"))
                {
                    //Clean out any attributes from past iterations
                    attributes.Clear();
 
                    xPathLocation = reader.GetAttribute("XPathLocation");
                    elementName = reader.GetAttribute("ElementName");
                    
                    //Make sure we have at least a path and element
                    if (xPathLocation == null || elementName == null)
                        throw new Exception("WebConfigChange missing required XPathLocation or ElementName attributes");
 
                    //Get the Attributes to apply
                    if (reader.ReadToDescendant("Attribute"))
                    {
                        do
                        {
                            attributes.Add(reader.GetAttribute("Name"), reader.GetAttribute("Value"));
                        } while (reader.ReadToNextSibling("Attribute"));
                    }
 
                    //Do the update
                    UpdateWebConfig(xPathLocation, elementName, attributes, removeModification);
                } 
            }
        }
        
        private void UpdateWebConfig(string XPathLocation, string ElementName,  
            Dictionary<string, string> Attributes, bool removeModification)
        {
            try
            {
                SPWebApplication webApp = null;
 
                //Get the web app
                //First check if it was deployed to a Site Collection
                SPSiteCollection siteCol = _properties.Feature.Parent as SPSiteCollection;
                if (siteCol == null)
                {
                    //Check if it was deployed to a site
                    SPSite site = _properties.Feature.Parent as SPSite;
                    if (site == null)
                    {
                        //Check if it was deployed to a Site
                        SPWeb web = _properties.Feature.Parent as SPWeb;
                        if (web != null)
                            webApp = web.Site.WebApplication;
                    }
                    else
                        webApp = SPWebApplication.Lookup(new Uri(site.Url));
                }
                else
                    webApp = siteCol.WebApplication;
 
                if (webApp != null)
                {
                    SPWebConfigModification modification =
                        new SPWebConfigModification(ElementName + CreateAttributeString(Attributes), XPathLocation);
                        
                    modification.Owner = "Company.Moss.FeatureReceiver.CustomFeatureReceiver";
                    modification.Sequence = 0;
                    modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
                    modification.Value =
                         string.Format(CultureInfo.InvariantCulture,
                         CreateModificationValueString(ElementName, Attributes),
                         CreateModificationValueArgs(Attributes));
 
                    if (removeModification)
                        webApp.WebConfigModifications.Remove(modification);
                    else
                        webApp.WebConfigModifications.Add(modification);
 
                    SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
                }
                else
                    throw new ApplicationException("Could not locate a web application");
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = "WebConfigFeature";
                el.WriteEntry(ex.Message);
            }
        }
 
        /// <summary>
        /// Accepts a dictionary object with all of the attributes for the web modification and
        /// creates a string representing the attribute values which can be used when creating
        /// the SPWebConfigModification object.
        /// </summary>
        /// <param name="Attributes"></param>
        /// <returns></returns>
        private string CreateAttributeString(Dictionary<string, string> Attributes)
        {
            //Create a string that looks like this (no line breaks):
            //[@Assembly=\"Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4\"]
            //[@Namespace=\"Company.Moss.Activities\"]
            //[@TypeName=\"*\"][@Authorized=\"True\"]
 
            string result = "";
 
            //Check if there are attributes
            if (Attributes.Count > 0)
            {
                foreach (KeyValuePair<string, string> kvp in Attributes)
                {
                    result += "[@" + kvp.Key + "=\"" + kvp.Value + "\"]";
                }
            }
            
            return result;
        }
 
        private string CreateModificationValueString(string ElementName, Dictionary<string, string> Attributes)
        {
            //Create a string that looks like this:
            //"<authorizedType Assembly=\"{0}\" Namespace=\"{1}\" TypeName=\"{2}\" Authorized=\"{3}\"/>"
 
            string result = "<" + ElementName;
 
            //Check if there are attributes (Kind of silly if there aren't!)
            if (Attributes.Count > 0)
            {
                int i = 0;
                foreach (string key in Attributes.Keys)
                {
                    result += " " + key + "=\"{" + i.ToString() + "}\"";
                    i++;
                }
            }
 
            result += " />";
 
            return result;
        }
 
        private object[] CreateModificationValueArgs(Dictionary<string, string> Attributes)
        {
            //Create an object that looks like this:
            //"Company.Moss.Activities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4", "Company.Moss.Activities", "*", "True"
            object[] result = new object[Attributes.Count];
            
            int i = 0;
            foreach(string value in Attributes.Values)
            {
                result[i] = value;
                i++;
            }
 
            return result;
        }
    }
}
Posted 22 March 08 03:32 by ryanm1201 | 7 Comments   
Filed under
More Posts Next page »