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
- App Path: Path of SharePoint App package example: "C:\Users\Rahul Jain\Desktop\FirstArticle\article-template\O365Apps.app".
- 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".
- Client Id: Guid that we generate while registering the app package example: "
d3c31d7d-3f3c-4dda-89f7-bcecbd9bbc12
". - Remote end points: Comma separated URL that we want to authenticate for SharePoint example: "http://domain/".
- App Version: Updated app version.
- Create App package: Button to update app properties.
- 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.
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.
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());
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.
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