Create a List in the Host Web When Your SharePoint App is Installed...and Remove it from the Recent Stuff List

This is one of those questions that I've seen asked a lot but for some reason I've never really seen anyone post an answer to it. I did a quick Bing search before writing this post but didn't really see anything up there so I'm going to go out on a bit of a limb and guess that maybe there's still some use for this kind of information. In my particular scenario I am creating a SharePoint app and when it's installed I want it to create a list in the host web, because I'll be using it to store data for my application. I'm using a provider hosted app so I'm not going to have an app web, but even if I DID have an app web, I'd rather have this list in the host web in case my app is ever uninstalled and then reinstalled later, my application data will still be there and I can pick right up where I left off.

So to get started on this little adventure, I created a new SharePoint application in Visual Studio 2013. It began as just your basic out of the box application - I just wanted to get it up and working first to verify that all the plumbing is in place. With that ready to go, the next thing I wanted to do is have something similar to a feature receiver that you have for full trust code that would fire when my app is installed. Fortunately the cloud app model provides just such a thing. I just needed to select my SharePoint App project in Visual Studio and in the properties windows there is a property called HandleAppInstalled; just double-click that to change it from it's default value of false to true and when you do a new WCF endpoint is added to your provider hosted app. It's all fleshed out with a basic stub implementation, and then you can add your code in there. That's what we'll begin with.

The stub implementation looks something like this:

public SPRemoteEventResult ProcessEvent(SPRemoteEventProperties properties)
{
SPRemoteEventResult result = new SPRemoteEventResult();

using (ClientContext ctx = TokenHelper.CreateAppEventClientContext(properties, false))
{
//blah blah blah
}

return result;
}

Now the challenge is that the CreateAppEventClientContext is geared towards creating a context for an app web, which we don't want. So instead we're going to create a client context the way that Visual Studio 2012 did when you create a provider hosted app, in the default.aspx code behind. That means we're going to need the Url to the host web, a context token, and the host name of our application. Here's how we're going to get all of that stuff.

To begin with, you can get the Url to the host web in the SPRemoteEventProperties parameter that is passed into our method. The properties parameter has an object property called AppEventProperties that contains a URI property called HostWebFullUrl. So we can get the first parameter we need - the host web Url - like this: Uri hostUrl = properties.AppEventProperties.HostWebFullUrl. The context token is actually very easy to get; it's a property of the properties parameter, so we can get it like this: properties.ContextToken. The last thing we need - the host name of our application - is more difficult to get in a WCF. That's because you don't have an HttpContext in a WCF, so you have to reach back into the ServiceContext to get the information you're looking for. Fortunately the ServiceContext contains a Host property that has a collection of URIs in the BaseAddresses property. Here's a chunk of code to pull out that information:

System.ServiceModel.OperationContext oc = System.ServiceModel.OperationContext.Current;

Uri localUrl = null;

//enumerate through the Host base addresses and look for the SSL connection
foreach (Uri u in oc.Host.BaseAddresses)
{
if (u.Scheme.ToLower() == "https")
{
localUrl = u;
break;
}
}

UPDATE: The code above looking through the BaseAddresses works great when you're using it with the IIS Express web site Visual Studio adds to your project when you create a new SharePoint App. HOWEVER...it blows up when you move your code to the full blown version of IIS. What I found in trying to get this working is that the you still get two BaseAddresses; the first one uses the HTTP scheme but IS the correct host name for your WCF service. The second one uses the HTTPS scheme, but is the fully qualified domain name of the server on which the code is running. As a result, the code above picks the second host name, but that is NOT the host name registered for the endpoint with SharePoint. As a result, the code below to get a client context fails and everything blows up at that point. NOTE: This is because my IIS web name is not the same as my machine name, which would generally be the case in a production environment. The code I'm using now that works with both IIS Express as well as IIS is just this:

if (oc.Host.BaseAddresses.Count > 0)
localUrl = oc.Host.BaseAddresses[0];

I'm looking for the host that is using HTTPS, since that's what our apps should always communicate over. It is arguably unlikely that you would have two different host names for HTTP and HTTPS, but you never know so why tempt fate? Now that I have the three parameters I need I can create my client context for the host web; note that I always FIRST check to make sure that localUrl is not null, which would happen if I had no SSL endpoints on my provider hosted app. Getting my client context now looks like this:

//this is what was originally here
//using (ClientContext ctx = TokenHelper.CreateAppEventClientContext(properties, false))
using (ClientContext ctx = TokenHelper.GetClientContextWithContextToken(hostUrl.ToString(), properties.ContextToken, localUrl.Authority))

Awesome! Now that I have a client context for my host web, as long as my user / application has sufficient rights I can create a new list in my host web. I'm not really going to cover that in great detail because I think creating lists via CSOM is all over the interwebs. But for completeness here's a shortened version of my code to create a new list:

Web web = ctx.Web;

ListCreationInformation ci = new ListCreationInformation();
ci.Title = LIST_NAME;
ci.TemplateType = (int)ListTemplateType.GenericList;
ci.QuickLaunchOption = QuickLaunchOptions.Off;

l = web.Lists.Add(ci);

//add a description and some fields in here
//blah

l.Hidden = true;
l.Update();

//this creates the list
ctx.ExecuteQuery();

Okay, all of that is great, I now have my list in my host web. There is one important aspect to this scenario though, and that is that my list should be hidden from users. Now you might think that I have that covered with my ListCreationInformation (ci.QuickLaunchOption) and the Hidden property of the list...but that is not enough. Unfortunately SharePoint still throws it in the Recent bucket that shows up on the Quick Launch navigation. In fact the Recent list is NOT a list, it's a collection of NavigationNode items. So if you REALLY want your list to be invisible, you need to remove it from over there as well. Doing that is three step process (NOTE: I'm leaving out all the error handling and try...catch blocks for read-ability; you would of course want that in any code you write...but you knew that, I know... :-) ):

Step 1: Get the quick launch navigation node collection

In this step you're going to make a call back to get the root web of the site to get the quick launch navigation nodes. This code is just continued from the create list code above, so it's still in the "using" statement for my ClientContext (ctx) shown above:

//get the site and root web, where the navigation lives
Site s = ctx.Site;
Web rw = s.RootWeb;

//get the QuickLaunch navigation, which is where the Recent nav lives
ctx.Load(rw, x => x.Navigation, x => x.Navigation.QuickLaunch);
ctx.ExecuteQuery();

//now extract the Recent navigation node from the collection
var vNode = from NavigationNode nn in rw.Navigation.QuickLaunch
where nn.Title == "Recent"
select nn;

NavigationNode nNode = vNode.First<NavigationNode>();

Step 2: Get the Child Nodes of the "Recent" Navigation Node

Okay, now that I have the "Recent" navigation node, I need to populate it's child property, which is where I should find a node for the list I just created. This gets simpler now, here is the code to retrieve that:

//now we need to get the child nodes of Recent, that's where our list should be found
ctx.Load(nNode.Children);
ctx.ExecuteQuery();

Step 3: Find The Node for the New List and Delete It

Now that I have the collection of items in the Recent navigation, I can find my item and delete it like you would any other item via CSOM. Here's the code:

var vcNode = from NavigationNode cn in nNode.Children
where cn.Title == LIST_NAME
select cn;

//now that we have the node representing our list, delete it
NavigationNode cNode = vcNode.First<NavigationNode>();
cNode.DeleteObject();

ctx.ExecuteQuery();

And there you have it. You've now created a new list in the host web, made it hidden and not shown on the quick launch bar, and removed it from the Recent navigation list so it is REALLY out of sight.