.NET, Office Dev, Technical

Convert Office Add-In Web to MVC

Using Visual Studio to create a new Office Add-In results in two projects:  One for your Office Add-In (basically, the manifest) and another for the web project where you do the bulk of your work to implement functionality. The web project that is created is a basic HTML/JS/CSS application…nothing fancy like ASP.NET. For most situations, a lightweight client-side web application is ideal and it makes sense for that to be the default of the VS project template. How about those other situations where you need something to run on the server, like an ASP.NET MVC application? There are a couple choices:

  1. Add a new MVC project to your solution and pull in the Office “stuff” and trim it down so it can be used as the web project for your Add-In
  2. Convert the existing web project to MVC

I’ll show you how to do the second option in this post.

MVC Plumbing

  1. Using NuGet Package Manager, add Microsoft.AspNet.Mvc to your web project
  2. Add the following folders to your project:  App_Start, Controllers, Views
  3. Right-click the Views folder and add a new Web Configuration File and name it Web.config
  4. Add the following code to this new Web.config, replacing [[YourNamespace]] with the namespace of your project
       1: <?xml version="1.0"?>

       2:  

       3: <configuration>

       4:   <configSections>

       5:     <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">

       6:       <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />

       7:       <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />

       8:     </sectionGroup>

       9:   </configSections>

      10:  

      11:   <system.web.webPages.razor>

      12:     <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />

      13:     <pages pageBaseType="System.Web.Mvc.WebViewPage">

      14:       <namespaces>

      15:         <add namespace="System.Web.Mvc" />

      16:         <add namespace="System.Web.Mvc.Ajax" />

      17:         <add namespace="System.Web.Mvc.Html" />

      18:         <add namespace="System.Web.Routing" />

      19:         <add namespace="[[YourNamespace]]" />

      20:       </namespaces>

      21:     </pages>

      22:   </system.web.webPages.razor>

      23:  

      24:   <appSettings>

      25:     <add key="webpages:Enabled" value="false" />

      26:   </appSettings>

      27:  

      28:   <system.webServer>

      29:     <handlers>

      30:       <remove name="BlockViewHandler"/>

      31:       <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />

      32:     </handlers>

      33:   </system.webServer>

      34:  

      35:   <system.web>

      36:     <compilation>

      37:       <assemblies>

      38:         <add assembly="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />

      39:       </assemblies>

      40:     </compilation>

      41:   </system.web>

      42: </configuration>

CSS

I chose to use the Site.css stylesheet that the NuGet package created for me. To do that, I took all of the CSS from the app.css and the home.css file and put it in the Site.css file.

JavaScript

Copy the App.js and Home.js files to the Scripts directory.

Content

I’ll assume your existing web project has the standard files from the VS template:  app.js, app.css, home.html, home.css and home.js. This section shows how to pull that content into a new view.

  1. Right-click the Controllers folder and add a new Controller named HomeController. This should add a file that looks like the following:
       1: using System;

       2: using System.Collections.Generic;

       3: using System.Linq;

       4: using System.Web;

       5: using System.Web.Mvc;

       6:  

       7: namespace [[YourNamespace]].Controllers

       8: {

       9:     public class HomeController : Controller

      10:     {

      11:         // GET: Home

      12:         public ActionResult Index()

      13:         {

      14:             return View();

      15:         }

      16:     }

      17: }

  2. Add a Home and Shared folder to the Views folder
  3. Right-click the Shared folder and add a new View called _Layout using the “Empty (without model)” template and checking the box for “Create as a partial view”
    snip_20151111122614
  4. Replace the <head> content with the following (note the reference to the Office UI Fabric, that’s optional if you aren’t using it…but you should be):
       1: <meta charset="utf-8" />

       2: <meta name="viewport" content="width=device-width, initial-scale=1.0">

       3: <meta http-equiv="X-UA-Compatible" content="IE=Edge" />

       4: <title>@ViewBag.Title - My ASP.NET Application</title>

       5: <script src="~/Scripts/modernizr-2.6.2.js"></script>
       1:  

       2:  

       3: <link href="~/Content/Office.css" rel="stylesheet" />

       4: <script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js" type="text/javascript">

       1: </script>

       2: <script src="~/Scripts/jquery-1.10.2.min.js">

       1: </script>

       2:  

       3: <!-- Office UI Fabric -->

       4: <link rel="stylesheet" href="//appsforoffice.microsoft.com/fabric/1.0/fabric.min.css" />

       5: <link rel="stylesheet" href="//appsforoffice.microsoft.com/fabric/1.0/fabric.components.min.css" />

       6:  

       7: <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />

       8: <script src="~/Scripts/App.js">

       1: </script>

       2: <script src="~/Scripts/Home.js">

    </script>

  5. Replace the <body> content with the following:
       1: <div id="content-header">

       2:     <div class="padding">

       3:         <h1>[[Your application name]]</h1>

       4:     </div>

       5: </div>

       6:  

       7: <div id="content-main">

       8:  

       9:     @RenderBody()

      10:     <hr />

      11:     <footer>

      12:         <p>&copy; @DateTime.Now.Year - Content Mixr</p>

      13:     </footer>

      14: </div>

  6. If you have anything like a top nav or other content that is consistent across multiple pages in your app, paste it in there as appropriate
  7. Right-click the Home folder and add a new View called Index (or whatever you want it to be called, the rest of this post assumes Index) using the “Empty (without model)” template and checking the box for “Create as a partial view”
  8. Replace the contents of index.cshtml with the HTML from the <body> section of your home.html file (keep the ViewBag.Title at the top of the file if you’re going to use it)

Config and Cleanup

  1. Right-click the Views folder and add a new partial view called _ViewStart and add the following content:
       1: @{

       2:     Layout = "~/Views/Shared/_Layout.cshtml";

       3: }

  2. (This may already exist but create it if not) Right-click the App_Start folder and add a new class file called RouteConfig.cs and add the following content:
       1: using System;

       2: using System.Collections.Generic;

       3: using System.Linq;

       4: using System.Web;

       5: using System.Web.Mvc;

       6: using System.Web.Routing;

       7:  

       8: namespace [[Your application namespace]]

       9: {

      10:     public class RouteConfig

      11:     {

      12:         public static void RegisterRoutes(RouteCollection routes)

      13:         {

      14:             routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

      15:  

      16:             routes.MapRoute(

      17:                 name: "Default",

      18:                 url: "{controller}/{action}/{id}",

      19:                 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }

      20:             );

      21:         }

      22:     }

      23: }

  3. Open the Global.asax.cs file and edit the Application_Start() method to the following:
       1: protected void Application_Start()

       2: {

       3:     AreaRegistration.RegisterAllAreas();

       4:     RouteConfig.RegisterRoutes(RouteTable.Routes);

       5: }

  4. If your app doesn’t use Bootstrap, delete the Bootstrap CSS and JS files (if you are, go back to your _Layout.cshtml file and add the Bootstrap references as the above code doesn’t have it)
  5. You may have noticed the MVC package brought in a reference to a newer version of jQuery (1.10.2 as of the writing of this post.) There are now probably two jQuery versions in the Scripts folder so delete the version you don’t want.
  6. Open the App Manifest XML file (from the Office Add-In project) and set the DefaultValue for SourceLocation to ~remoteAppUrl since the default page of the app is now the web app default page
       1: <DefaultSettings>

       2:   <SourceLocation DefaultValue="~remoteAppUrl/" />

       3: </DefaultSettings>

That should do it. Please post a comment here or contact me directly if you hit any snags.

.NET, SharePoint, Technical

REST Calls and MDS

SharePoint 2013 introduces MDS, or Minimal Download Strategy, as a way to increase performance of various types of SharePoint sites (basically, non-publishing sites.) It’s turned on for quite a few site templates out of the box so it’s important to understand how it works. I won’t go into it here since others cover it well, but review the links at the bottom of this post if you’re interested.

When developing a custom master page, there’s a new control that you’ll need to be concerned with due to MDS called AjaxDelta. This control is used to tell MDS which controls or content that call into SharePoint require the user to be authenticated and authorized. Any content outside of an AjaxDelta control is treated anonymously. This may bite you if you forget and start moving controls around. An example of that can be found here.

If you haven’t put two and two together yet based on the title of this post, you’ll need to also wrap your client side REST calls with an AjaxDelta control. Well, maybe. I was getting pretty good results without the AjaxDelta wrapper, until I did some testing using Windows XP and IE 8. On that platform, my REST calls were resulting in “Access is Denied” entries in ULS logs in an unauthenticated server call (as shown by a “Authentication Authorization” entry where IsAuthenticated=false.) Once I put the script inside an AjaxDelta control, the problem went away.

<SharePoint:AjaxDelta ID="DeltaCustomJS" Container="false" runat="server">
     <script type="text/javascript" src="/SiteAssets/CustomBranding/custom.js"></script>
</SharePoint:AjaxDelta>

As mentioned, this was only for WinXP and IE8 so perhaps there is a client setting that can be changed to fix the issue as well.

Here’s a good link on MDS, if you’re interested (if you read this far, you’re interested Smile):

Wictor Wilen – Introduction to MDS

.NET, BizTalk, Business, InfoPath, Mobile, Personal, Photography, Politics, SharePoint, SQL Server, Technical, Windows Workflow

First WordPress Post

I finally moved off of Community Server as my blog engine and migrated to this site using WordPress.  It’s all new to me so I’m still figuring out the settings, but with the help of my hosting company (WebHost4Life.com) and Vincent (support guy), I was able to get all the content over (I think.)

I decided to move off CS for a couple reasons.  First, I was on an older version of the software and to upgrade would’ve been a pain in the ass.  Second, I initially created the site thinking it would be used to host blogs for other coworkers and friends, and post pictures.  It seemed like folks I ran into that were interested in blogging already had a blog, so it became my personal site.  As for pictures, I’m still trying to figure that solution out, but I know CS isn’t part of it.  Perhaps a newer version would’ve been useful, but see previous first point.  Lastly, although the CS setting for allowing comments was set, no one could comment on my posts.  Not what I wanted.  I realize I don’t have a lot of readers, but I’d like to have the option for communication with anyone that stops by.

Hopefully WordPress will last a while for me.  I’d, of course, like to get on a SharePoint platform some day, but that will have to wait.  If anyone has any WordPress tips, feel free to leave them in the comments (because you can do that now!)

.NET, SharePoint, Technical

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.

.NET, SharePoint, Technical

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!

.NET, SharePoint, Technical

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.)

.NET, InfoPath, SharePoint, Technical

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.

.NET, BizTalk, Business, Mobile, Personal, Photography, Politics, SharePoint, SQL Server, Technical, Windows Workflow

Test Twitter Notifier from Live Writer

.NET, SharePoint

Custom Code Access Security for Web Parts

We’ve been trying to figure out a way to do some custom searching in MOSS here for a couple weeks.  The first plan of attack was a custom Content Query Web Part using CommonViewFields hooked up to site columns based on a lookup.  Turns out there’s a bug doing that which doesn’t allow it to work as we wanted (maybe I’ll post about that later.)  That bug put a stop to the Content Query solution, so we moved on to another idea.

My friend and co-worker Brad Younge wrote a cool web part that does custom searching.  Two of them, actually.  One for custom queries and another for browsing content.  It’s a bit much for what we were trying to do, but once we show it to the customer I think we can modify it to their needs.

Once I got it up and running on my machine, I began getting security errors.  Like:

Request for the permission of type ‘Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c’ failed.

Took me a bit of searching to find the culprit.  The trust level on my site is set to WSS_Minimal.  Changing this to WSS_Medium or Full eliminated the errors, but changing the trust level in production wasn’t an option so I needed to go down the road of creating some custom code access security settings.  It’s not that difficult, but takes some time to track down what entries you need.  I ended up putting the following into my manifest.xml file:

    <CodeAccessSecurity>
        <
PolicyItem>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera BrowseForIt>
                <
IPermission class=AspNetHostingPermission version=1 Level=Minimal/>
                <
IPermission class=SecurityPermission version=1 Flags=Execution />
                <
IPermission class=System.Security.Permissions.RegistryPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Unrestricted=True />
                <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
Assemblies>
                    <
Assembly Name=Statera.Moss.Webparts.BrowseForIt Version=1.0.0.0 PublicKeyBlob=0x002400000480000094000000060200000024000052534131000400000100010031B4F1C0A8692A7AFA369B31D858618DBB26497585F791DD5B8BE9FD2AC7C9554B348BD97E37984CE645AECBE104E14AC99821C94CE0BF726459339D21A4EA9A1220BD7E4E858D9375C4D3B5EE98F3DA5D2D99C9628A03047E81CB409CB04A98633C6E2396E6289FF37A8781E6691697D48528C4B146CE631156F08A87309797 />
            </
Assemblies>
        </
PolicyItem>
        <
PolicyItem>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera Common>
                <
IPermission class=AspNetHostingPermission version=1 Level=Minimal/>
                <
IPermission class=SecurityPermission version=1 Flags=Execution />
                <
IPermission class=System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Read=USERNAME />
                <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
Assemblies>
                <
Assembly Name=Statera.Moss.Webparts.Common Version=1.0.0.0 PublicKeyBlob=0x00240000048000009400000006020000002400005253413100040000010001006B801BE53E218D8C0A003278A44D51BB14EDCD573981DD1288A7A492EF389D4BDC4D657F3F3CBF99222C34088AC56CC27FC7CC63821B67F0C70E1D1D7990DD80BCCCA0F455BB97908FFE41A7635172C7B5CAE4E923CE39A65FA5DED4498C750525CF997387F9C89DB3CFB644B97EC7EFACEBD5E081B237511CAADC8104380CDB />
            </
Assemblies>
        </
PolicyItem>
    </
CodeAccessSecurity>

When built into the .wsp and deployed with the -allowcaspolicies flag, it does a couple things.  First, it creates a custom trust config file called wss_custom_wss_minimaltrust.config.  That’s assuming your site is set to wss_minimal when you deploy.  If it’s set at wss_medium, the filename will be wss_custom_wss_mediumtrust.config.  It then sets the trust in web.config to WSS_Custom, changing it from the previous setting. 

When it creates the custom trust file, it takes the wss_minimaltrust.config file and adds content based on what’s in the manifest.  For this example, it adds two new PermissionSet entries with the listed IPermission mapping and settings.  

            <PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera Common Name=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-2>
              <
IPermission class=AspNetHostingPermission version=1 Level=Minimal />
              <
IPermission class=SecurityPermission version=1 Flags=Execution />
              <
IPermission class=System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Read=USERNAME />
              <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera BrowseForIt Name=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-1>
              <
IPermission class=AspNetHostingPermission version=1 Level=Minimal />
              <
IPermission class=SecurityPermission version=1 Flags=Execution />
              <
IPermission class=System.Security.Permissions.RegistryPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Unrestricted=True />
              <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>

The other change to the file takes place down in the CodeGroup section, where it adds one UnionCodeGroup class CodeGroup for each PermissionSet.  These CodeGroups point back to the PermissionSet so WSS knows which policies to apply when code in the listed assembly is run.  

            <CodeGroup class=UnionCodeGroup version=1 PermissionSetName=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-2>
              <
IMembershipCondition version=1 Name=Statera.Moss.Webparts.Common class=StrongNameMembershipCondition PublicKeyBlob=0x00240000048000009400000006020000002400005253413100040000010001006B801BE53E218D8C0A003278A44D51BB14EDCD573981DD1288A7A492EF389D4BDC4D657F3F3CBF99222C34088AC56CC27FC7CC63821B67F0C70E1D1D7990DD80BCCCA0F455BB97908FFE41A7635172C7B5CAE4E923CE39A65FA5DED4498C750525CF997387F9C89DB3CFB644B97EC7EFACEBD5E081B237511CAADC8104380CDB AssemblyVersion=1.0.0.0 />
            </
CodeGroup>
            <
CodeGroup class=UnionCodeGroup version=1 PermissionSetName=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-1>
              <
IMembershipCondition version=1 Name=Statera.Moss.Webparts.BrowseForIt class=StrongNameMembershipCondition PublicKeyBlob=0x002400000480000094000000060200000024000052534131000400000100010031B4F1C0A8692A7AFA369B31D858618DBB26497585F791DD5B8BE9FD2AC7C9554B348BD97E37984CE645AECBE104E14AC99821C94CE0BF726459339D21A4EA9A1220BD7E4E858D9375C4D3B5EE98F3DA5D2D99C9628A03047E81CB409CB04A98633C6E2396E6289FF37A8781E6691697D48528C4B146CE631156F08A87309797 AssemblyVersion=1.0.0.0 />
            </
CodeGroup>

I think that about covers it.  In one of the articles I read covering code access security the author mentioned every web part developer runs into this at some point, and once you figure it out you don’t forget.  I hope he’s right!  Oh, and here’s a good MSDN article covering the topic.

.NET, SharePoint, Windows Workflow

How to Deploy a Custom Activity as a Feature

Henry asked me to figure out a way to easily deploy the custom activity I wrote, so with a suggestion and a pointer from him I was on my way.  The end result is deployment of the activity as a feature.  Here’s how it’s done.

The concept is simple.  Use the normal Feature deployment framework to do the three things needed to deploy the activity.  Those are:

  1. Deploy the dll to the GAC
  2. Add the .ACTIONS file to the 12 hive
  3. Update the authorizedTypes in the web.config

The first two are pretty simple, but I’ll show the manifest so you get an idea if you’re not sure how that works.

<Solution SolutionId="153F5D62-5662-4288-B915-903F5B950E39" xmlns="http://schemas.microsoft.com/sharepoint/" ResetWebServer="TRUE">

<
FeatureManifests>
<
FeatureManifest Location="CustomActivitiesFeature.xml"/>
</
FeatureManifests>

<
RootFiles>
<
RootFile Location="TEMPLATE1033WorkflowFindManager.ACTIONS" />
</
RootFiles>

<
Assemblies>
<
Assembly DeploymentTarget="GlobalAssemblyCache" Location="MyAssembly.dll">
<
SafeControls>
<
SafeControl Assembly="MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4" Namespace="MyNamespace" TypeName="*" />
</
SafeControls>
</
Assembly>
</
Assemblies>
</
Solution>

To update the authorizedTypes in the web.config I needed some help (remember, I’m new!)  Henry pointed me to the SPWebConfigModification class and the concept of an SPFeatureReceiver.  The theory was to use an SPFeatureReceiver and override the FeatureActivated method and use that to call into SPWebConfigModification to update the web.config file with the new authorizedType entry.  So with that, I was off!!

Other than the MSDN documentation, I found a couple other valuable resources.  The first was a post by Tony Bierman on Using SPWebConfigModification to modify a SharePoint web application’s web.config for all servers in the farm.  Sounds like what I need, right?!  Just about.  The biggest thing missing was how to get the right SPWebApplication reference.  His example was using a console app in which the web app was passed in as a parameter.  I played around with SPContext (not sure why, since there’s no http context during deployment of a wsp), as well as different options using the Parent of the Feature.  I never quite figured it out.  Then I ran into Daniel Larson’s posting on the same subject.  (I think he and I ran into each other a few years ago on a project in Parker, by the way.  I’m not sure.)

Dan was grabbing the web app using the following:

        SPWebApplication app = null;
        SPSiteCollection site = properties.Feature.Parent as SPSiteCollection;
        if (site == null)
        {
            SPWeb web = properties.Feature.Parent as SPWeb;
            if (web != null)
                app = web.Site.WebApplication;
        }
        else
            app = site.WebApplication;

Basically, using properties.Feature.Parent, as I was attempting.  He first checks to see if it’s a Site Collection and then, if not, assumes it’s a Web.  Well, guess what?  I wasn’t deploying my feature as either!  Maybe I need to rethink that, but I’m deploying it to a specific Site.  This helped me get to the right web application by doing:

        SPSite site = (SPSite)properties.Feature.Parent;
        SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

After that I needed to spend some time reviewing how SPWebConfigModification works.  I started out by creating a modification object for my authorizedTypes entry and calling webApp.WebConfigModifications.Add, but that didn’t work.  If you take a look at the MSDN docs you’ll see why I started there.  I was missing a few steps before I could call .Add.  I needed to add a few more settings to the modification, as you can see in the code below.  That’s about it for the tricky stuff, so I’ll post the code.  One more thing to notice, though, is that I also overrode FeatureDeactivating to remove the .config entry.

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSite site = (SPSite)properties.Feature.Parent;
                SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

                if (webApp != null)
                    UpdateWebConfig(webApp, false);
                else
                    throw new ApplicationException(“Could not locate a web application”);
               
                //Write log entry for success
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(“Activation complete.”);
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(ex.Message);
            }
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSite site = (SPSite)properties.Feature.Parent;
                SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

                if (webApp != null)
                    UpdateWebConfig(webApp, true);
                else
                    throw new ApplicationException(“Could not locate a web application”);

                //Write log entry for success
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(“De-Activation complete.”);
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(ex.Message);
            }
        }

        private static void UpdateWebConfig(SPWebApplication webApp, bool removeModification)
        {
            SPWebConfigModification modification =
                new SPWebConfigModification(“authorizedType[@Assembly=”MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4″][@Namespace=”MyNamespace”][@TypeName=”*”][@Authorized=”True”]”, “configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes”);
         
            modification.Owner = “Ryan”;
            modification.Sequence = 0;
            modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
            modification.Value =
                 string.Format(CultureInfo.InvariantCulture,
                 “<authorizedType Assembly=”{0}” Namespace=”{1}” TypeName=”{2}” Authorized=”{3}”/>”,
                 new object[] { “MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4”, “MyNamespace”, “*”, “True” });

            if (removeModification)
                webApp.WebConfigModifications.Remove(modification);
            else
                webApp.WebConfigModifications.Add(modification);

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