Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / SharePoint

Dynamically Update SharePoint App Package

5.00/5 (1 vote)
19 May 2015CPOL3 min read 9.7K   140  
This tip explains how we can modify SharePoint App package properties like Remote end points, app clientId, and Custom action URL, etc.

Sample Image - maximum width is 600 pixels

Introduction

This tip explains how can we modify SharePoint App (Provider or SharePoint Hosted) package from C# code without the help of Visual Studio.

Background

In the last few months, I was working on SharePoint App. There, I came across a situation where I needed to modify the App property like Remote end points, Ribbon URL and lots more in App catalog for different customers. I searched and got nothing as Microsoft suggested that you can't modify app properties from App catalog or from stores. But I had to provide a way to modify the app properties.

So first, I tried by simply changing the extension of app package (.app) to .zip and then unzipping the content, then I saw my app manifest there which contains the app properties, then I modified the app manifest again zipping the files and changed the extension to .app and tried to upload the package. However, I wasn't successful because the app package is not the simple zip package. It is the Cabinet package which has .cab extension and it has different compression than zip.

Then, I tried the same thing with a tool (IZArc) which makes the cabinet file of your zip and then I made the package and successfully uploaded the same.

But I have to find a way to modify the app properties from the app UI itself. So, I finally decided to provide an app page that takes the app package (that you want to modify), parameters that you want to modify and then simply a button click will give you the new app package that you can update in app catalog with updated properties.

Using the Code

Form Parameters

  1. App Path: Path of SharePoint App package example: "C:\Users\Rahul Jain\Desktop\FirstArticle\article-template\O365Apps.app".
  2. App Host Url: Url of the website where web project is deployed. It can be of Azure or any other domain, example: "https://test.azurewebsites.net".
  3. Client Id: Guid that we generate while registering the app package example: "d3c31d7d-3f3c-4dda-89f7-bcecbd9bbc12".
  4. Remote end points: Comma separated URL that we want to authenticate for SharePoint example: "http://domain/".
  5. App Version: Updated app version.
  6. Create App package: Button to update app properties.
  7. Output File Path: Path of newly generated app package.

In this code sample, I have created one Windows Form application that uses System.IO.Compression DLL to zip and unzip the app file.

I have one PublishAppPackage method which takes the app package path and then create a temp folder and copy the app package to cab file that we have created and then use ZipArchive method of System.IO.Compression DLL to update the app manifest file that is in cab file. We have created two other methods to update app manifest and custom action method, save the cab file as app file and return the Output file path.

C#
// This method copy the app file data to temp cab file.
// call the update app manifest and custom action method.
// and the save the cab file as app file.
public string PublishAppPackage(string appPath)
    {
        string tempDir = string.Empty;
        string outPutFile = string.Empty;
        try
        {
            string appPackageName = System.IO.Path.GetFileName(appPath);
            string parentDir = System.IO.Path.GetDirectoryName(appPath);
            outPutFile = System.IO.Path.Combine(parentDir, "temp\\" + appPackageName);
            tempDir = System.IO.Path.Combine(parentDir, "temp\\tempdir");

            Directory.CreateDirectory(tempDir);

            string cabPath = System.IO.Path.Combine
               (tempDir, System.IO.Path.GetFileNameWithoutExtension(appPath) + ".cab");
            FileInfo fInfo = new FileInfo(appPath) { IsReadOnly = false };
            File.Copy(appPath, cabPath);

            string appManifest = string.Empty;
            string ribbonCustomAction = string.Empty;

            var appHostUrl = textBox2.Text;

            using (ZipArchive zipArch = ZipFile.Open(cabPath, ZipArchiveMode.Update))
            {
                appManifest = string.Format(@"{0}\AppManifest.xml",
                              Directory.GetParent(cabPath).FullName);
                UpdateAppManifest(zipArch, appManifest, appHostUrl);

                ribbonCustomAction = string.Format(@"{0}\elements.xml",
                                     Directory.GetParent(cabPath).FullName);
                UpdateRibbonFile(zipArch, ribbonCustomAction, appHostUrl);
            }

            File.Delete(appManifest);
            File.Delete(ribbonCustomAction);

            if (File.Exists(outPutFile))
            {
                File.Delete(outPutFile);
            }

            File.Move(cabPath, outPutFile);
            return outPutFile;
        }
        catch { }
        finally
        {
            if (System.IO.Directory.Exists(tempDir))
            {
                System.IO.Directory.Delete(tempDir, true);
            }
        }
        return outPutFile;
    }

As our app manifest is an XML file so I have used XDocument class of System.Xml.Linq to modify the app manifest. This method update the app manifest file with the form properties.

C#
// This method update the app manifest file with the form properties.
private void UpdateAppManifest(ZipArchive zipArch, string appManifest, string appHostUrl)
    {
        ZipArchiveEntry manifestEntry = zipArch.Entries.LastOrDefault
                                        (e => e.Name.ToLower() == "appmanifest.xml");
        manifestEntry.ExtractToFile(appManifest);
        XDocument doc = XDocument.Load(appManifest);
        XNamespace ns = doc.Root.GetDefaultNamespace();

        doc.Descendants(XName.Get("App",
            ns.NamespaceName)).First().Attribute(XName.Get("Version")).Value = UpdateAppVersion();

        doc.Descendants(XName.Get("StartPage",
            ns.NamespaceName)).First().Value = appHostUrl + "/Pages/Default.aspx?{StandardTokens}";
        doc.Descendants(XName.Get("InstalledEventEndpoint",
            ns.NamespaceName)).First().Value = appHostUrl + "/Services/AppEventReceiver.svc";
        doc.Descendants(XName.Get("UninstallingEventEndpoint",
            ns.NamespaceName)).First().Value = appHostUrl + "/Services/AppEventReceiver.svc";
        doc.Descendants(XName.Get("UpgradedEventEndpoint",
            ns.NamespaceName)).First().Value = appHostUrl + "/Services/AppEventReceiver.svc";

        doc.Descendants(XName.Get("RemoteWebApplication", ns.NamespaceName)).First().Attribute
            (XName.Get("ClientId")).Value = Convert.ToString(textBoxClientId.Text);

        XElement remoteEndPoints = doc.Descendants
           (XName.Get("RemoteEndpoints", ns.NamespaceName)).FirstOrDefault();
        if (remoteEndPoints == null)
        {
            remoteEndPoints = new XElement("RemoteEndpoints");
            doc.Add(remoteEndPoints);
        }

        doc.Descendants(XName.Get
          ("RemoteEndpoint", ns.NamespaceName)).ToList().ForEach(rep => rep.Remove());

        // get all dbxl instances
        var dbxlInstances = GetREP();

        foreach (var instances in dbxlInstances)
        {
            remoteEndPoints.Add(new XElement(XName.Get
                ("RemoteEndpoint", ns.NamespaceName), new XAttribute("Url", instances)));
        }

        doc.Save(appManifest);
        if (manifestEntry != null)
        {
            manifestEntry.Delete();
        }

        zipArch.CreateEntryFromFile(appManifest, "AppManifest.xml");
    }

Suppose you have any Custom action for custom buttons on ribbon or ECB Custom action and you want to modify the URL of it, then I have created one function for this. In our sample, I have replaced the Url with the app host Url that we have entered in Form.

For custom action element file, you have to find the file with contains because in app package, you will find element file name something like this "elementsdf0c4b89-2537-426e-9443-2630b639dec2.xml". Here, it will append a feature guid with element file.

C#
// This method find element file and update the Url of custom action with app host Url.
private static void UpdateRibbonFile
       (ZipArchive zipArch, string ribbonCustomAction, string appHostUrl)
    {
        ZipArchiveEntry ribbonCustomActionEntry =
            zipArch.Entries.LastOrDefault(e => e.Name.ToLower().Contains("elements"));
        if (ribbonCustomActionEntry == null)
            return;

        ribbonCustomActionEntry.ExtractToFile(ribbonCustomAction);

        XDocument doc = XDocument.Load(ribbonCustomAction);
        XNamespace ns = doc.Root.GetDefaultNamespace();

        doc.Descendants(XName.Get("CommandUIHandler", ns.NamespaceName)).First().Attribute
        (XName.Get("CommandAction")).Value = appHostUrl + "/Pages/DbxlListSettings.aspx?
        {StandardTokens}&SPListItemId={SelectedItemId}&SPListId={SelectedListId}";

        doc.Save(ribbonCustomAction);
        if (ribbonCustomActionEntry != null)
        {
            ribbonCustomActionEntry.Delete();
        }

        zipArch.CreateEntryFromFile(ribbonCustomAction, ribbonCustomActionEntry.FullName);
    }

History

  • 17th May, 2015: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)