Extend the Newsfeed Functionality of SharePoint 2013

Requirement: Provide ability to “one-click” post content to newsfeed or site feeds that users have access to.

postbutton

Solution:

  • SharePoint Provider-Hosted App
    • default.aspx – a custom page to allow users entering content and post to their newsfeed or site feed.
    • postnewsfeed
    • Custom Action – add “Post” to ECB menu of target lists
      <?xml version="1.0" encoding="utf-8"?>
      <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
       <CustomAction Id="c72a6c2f-b61e-41bc-90ec-4d7000aa4a64.PostToNewsfeed"
                     RegistrationType="List"
                     RegistrationId="100"
                     Location="EditControlBlock"
                     Sequence="20"
                     Title="$Resources:profilebrowserscriptres,PostCommandText"
                     HostWebDialog="true"
                     HostWebDialogHeight="420"
                     HostWebDialogWidth="500">
      
          <UrlAction Url="~remoteAppUrl/Pages/Default.aspx?{StandardTokens}&amp;SPListItemId={ItemId}&amp;SPListId={ListId}" />
       </CustomAction>
  • JQuery UI AutoComplete
    • Generating mention and hashtags via JSOMmention
/// <summary>
/// WebMethod to search people for metion
/// </summary>
/// <param name="spHostUrl"></param>
/// <param name="term"></param>
/// <returns>People Search result in an array contains AccountName and PreferredName</returns>
[WebMethod]
[System.Web.Script.Services.ScriptMethod]
public static string[] SearchPeople(string spHostUrl, string term)
{

   List<string> searchResults = new List<string>();

   try
   {  
     using (var ctx = new ClientContext(spHostUrl))
     {
       if (ctx != null && term != null)
       {
         KeywordQuery query = new KeywordQuery(ctx);
         query.QueryText = "(PreferredName:" + term + "* AND LastName:" + term + "*)";
         query.SourceId = new Guid("B09A7990-05EA-4AF9-81EF-EDFAB16C4E31"); // this is the People result source
         query.RowLimit = 10;
         query.TrimDuplicates = true;
         query.EnableSorting = true;
         query.SelectProperties.Add("AccountName");
         query.SelectProperties.Add("PreferredName");
         query.SortList.Add("LastName", Microsoft.SharePoint.Client.Search.Query.SortDirection.Ascending);
 
         SearchExecutor searchExecutor = new SearchExecutor(ctx);

         ClientResult<ResultTableCollection> results = searchExecutor.ExecuteQuery(query);
 
         ctx.ExecuteQuery();

         #region loop through results
         if (results.Value.Count > 0)
         { 
           for (int i = 0; i < results.Value[0].ResultRows.Count(); i++)
           {
             var resultRow = results.Value[0].ResultRows.ElementAt(i);
             string name = resultRow["PreferredName"].ToString();
             string accountName = resultRow["AccountName"].ToString();
             searchResults.Add(name + "|" + accountName);
           }
         }
         #endregion
       }
     }
   }
   catch (Exception ex)
   {
     Logger.Log(ex);
   }

   return searchResults.ToArray();
}
/// <summary>
/// WCF Service method to retrieve Hashtags
/// </summary>
/// <param name="spHostUrl"></param>
/// <param name="term"></param>
/// <returns></returns>

public string[] SearchHashTag(string spHostUrl, string term)
{
   List<string> searchResults = new List<string>();
   try
   {
     using (var ctx = TokenHelper.GetS2SClientContextWithWindowsIdentity(new Uri(spHostUrl), null))
     {
       if (ctx != null && term != null)
       {
         TaxonomySession taxonomySession = TaxonomySession.GetTaxonomySession(ctx);
         TermStore termStore = taxonomySession.GetDefaultSiteCollectionTermStore();
         TermSetCollection termSetCollection = termStore.GetTermSetsByName("Hashtags", 1033);
         TermSet termSet = termSetCollection.GetByName("Hashtags");

         TermCollection termCollection = termSet.GetAllTerms();
         ctx.Load(termCollection);
         ctx.ExecuteQuery();

         #region loop through results
         if (termCollection.Count > 0)
         {
           var terms = termCollection.GetEnumerator();
           while (terms.MoveNext())
           {
             searchResults.Add(terms.Current.Name);
           }
         }
         #endregion
       }
     }

   }
   catch(Exception ex)
   {
     Logger.Log(ex);
   }

   return searchResults.ToArray();
}

Add Site Setting and Site Action from CSOM

Cited: FTC to CAM – Custom actions and property bag entries from SP App

// Add site settings link
UserCustomAction siteSettingLink = clientContext.Web.UserCustomActions.Add();
siteSettingLink.Group = "SiteTasks";
siteSettingLink.Location = "Microsoft.SharePoint.SiteSettings";
siteSettingLink.Name = "Sample_CustomSiteSetting";
siteSettingLink.Sequence = 1000;
siteSettingLink.Url = string.Format(DeployManager.appUrl, clientContext.Url);
siteSettingLink.Title = "Modify Site Metadata";
siteSettingLink.Update();
clientContext.ExecuteQuery();


// Add site actions link
UserCustomAction siteAction = clientContext.Web.UserCustomActions.Add();
siteAction.Group = "SiteActions";
siteAction.Location = "Microsoft.SharePoint.StandardMenu";
siteAction.Name = "Sample_CustomAction";
siteAction.Sequence = 1000;
siteAction.Url = string.Format(DeployManager.appUrl, clientContext.Url); ;
siteAction.Title = "Modify Site Metadata";
siteAction.Update();
clientContext.ExecuteQuery();

Make Property Bags Searchable in SharePoint 2013

A new cool thing in SharePoint 2013 is adding properties to a property bag and getting them indexed and searchable. SharePoint 2013 makes this possible, not only for webs, but even all the way down to an SPList.

Here is the PowerShell you can use to create an “Searchable” property in an SPWeb property bag:

$web = Get-SPWeb http://mytest.com
$web.AllProperties["Searchable"] = "Yes"
$web.IndexedPropertyKeys.Add("Searchable")
$web.Update()

Here is another exmample for adding an indexable property to a list:

$list = $web.Lists["Announcements"]
$folder = $list.RootFolder
$folder.Properties["ListSearchable"] = "Cool"
$folder.Update()
$list.IndexedRootFolderPropertyKeys.Add("ListSearchable")
$list.Update()

Then, you just need a Full or incremental crawl.  Make sure the crawled property is created in the search service application. Create Managed Property that maps to the crawled property.

Note:  The IndexedPropertyKey value is also saved in AllProperties using key (vti_indexedpropertykeys).  The value is base 64 encoded so if you want to update it, you’ll need to decode from and encode to base 64 string. However, this SPWeb.IndexedPropertyKeys property is not available for CSOM. Vesa provides an alternative solution in his post FTC to CAM – Setting indexed property bag keys using CSOM

// Used to convert the list of property keys is required format for listing keys to be index
public static string GetEncodedValueForSearchIndexProperty(List keys)
{
    StringBuilder stringBuilder = new StringBuilder();
    foreach (string current in keys)
    {
        stringBuilder.Append(Convert.ToBase64String(Encoding.Unicode.GetBytes(current)));
        stringBuilder.Append('|');
    }
    return stringBuilder.ToString();
}
// Decode the IndexedPropertyKeys value so it's readable
public static List GetDecodeValueForSearchIndexProperty(string encodedValue)
{
     List decodedKeys = new List();
     string[] keys = encodedValue.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
            
     foreach (string current in keys)
     {
          decodedKeys.Add(Encoding.Unicode.GetString(Convert.FromBase64String(current)));
     }

     return decodedKeys;
}

Update Property Bag – Add / Remove

string indexedPropertyKeys = web.AllProperties["vti_indexedpropertykeys"].ToString();

List decodedKeys = GetDecodeValueForSearchIndexProperty(indexedPropertyKeys);
decodedKeys.Add(key);     //decodedKeys.Remove(key);

web.AllProperties["vti_indexedpropertykeys"] = GetEncodedValueForSearchIndexProperty(decodedKeys);
web.Update();
clientContext.ExecuteQuery();

Apply Custom Theme to SharePoint Site via CSOM

public static void ApplyTheme(ClientContext clientContext, string colorFilePath)
{
  Site site = clientContext.Site;
  Web rootWeb = site.RootWeb;
  Web currentWeb = clientContext.Web;
  clientContext.Load(site, s => s.ServerRelativeUrl);
  clientContext.Load(rootWeb, rw => rw.Language, rw => rw.Id);
  clientContext.Load(currentWeb, cw => cw.Id);
  clientContext.ExecuteQuery();

  string siteUrl = (site.ServerRelativeUrl == "/") ? String.Empty : site.ServerRelativeUrl;
  string colourFileUrl = String.Format("{0}/_catalogs/theme/15/claret.spcolor", siteUrl);

  if (rootWeb.Id.ToString() == currentWeb.Id.ToString())
  {
    List themeList = clientContext.Site.GetCatalog(Constants.ThemeCatalog);
    Folder subFolder = rootWeb.GetFolderByServerRelativeUrl(String.Format("{0}/_catalogs/theme/15", siteUrl));

    FileCreationInformation colorFile = new FileCreationInformation();
    colorFile.Content = System.IO.File.ReadAllBytes(colorFilePath);
    colorFile.Url = "claret.spcolor";
    colorFile.Overwrite = true;

    Microsoft.SharePoint.Client.File colorFileToUpload = subFolder.Files.Add(colorFile);

    clientContext.Load(colorFileToUpload);
  }

  currentWeb.ApplyTheme(colourFileUrl, null, null, true);
}

Creating Self-Signed (domain) Certificate for Provider-Hosted App

This is the certificate that will be used on the IIS site to make it SSL enabled site.  It’s different from the one to create the STS Security token (High Trust Provider-Hosted App Solution).  If the certificate is not created with proper domain associated, you’ll have issues with calling the App Event Receivers.  To create the certificate and set it up on IIS, following the instruction below:

  1. Open Visual Studio Developer Command Prompt and type the following command

(1) Create new certificate

makecert -r -pe -n "CN=devapps.pam.com" -b 01/01/2013 -e 01/11/2015 -eku 1.3.6.1.5.5.7.3.1 -ss my -sr localMachine -sky exchange -sy 12 -sp "Microsoft RSA SChannel Cryptographic Provider" "D:\SSLCerts\SPAppCertDev.cer"

(2) Add new certificate

certmgr /add "D:\SSLCerts\SPAppCertDev.cer" /s /r localMachine root

*** You need to replace the domain (devapps.pam.com) with your domain and the file path for the new certificate.

  1. Open MMC.exe and add Certificates snap-in
    •  Copy the new certificate from “Personal/Certificates” folder to “Trusted Root Certification Authorities/Certificates” folder.
  2. Open IIS Manager
    • Verify your domain certificate is added
    • Bind it to your IIS site
  3. Create Trust Root Authority on SharePoint Central Admin
    1. Open SharePoint Central Admin
    2. Click Security -> Manage Trust
    3. Click New
    4. Enter the information and browse to the newly create certificate on the page and save it.TrustRelationship

You can also run PowerShell Script to add a new trust relationship:

#Get the certificate from the hard drive
$publicCertificate = Get-PfxCertificate "SPAppCertDev.cer"
New-SPTrustedRootAuthority -Name "$($publicCertificate.Subject)_$($publicCertificate.Thumbprint)" -Certificate $publicCertificate

Creating Certificate on a Remote Server

If the server you’re on doesn’t have VS Studio installed, you can create the .cert and .pfx files on a different server and import them manually.

The script below will create the .cer and .pvk files.

makecert -r -pe -n "CN=devapps.*.pam.com" -b 01/01/2013 -e 01/11/2027 -sky exchange -sy 12 -sp "Microsoft RSA SChannel Cryptographic Provider" -sv "D:\SSLCerts\SPSiteTestDev.pvk" "D:\SSLCerts\SPSiteTestDev.cer"

The script below will create the .pfx file from the .cer and .pvk file. The .pfx is needed to import to IIS site.

pvk2pfx -pvk "D:\SSLCerts\SPSiteTestDev.pvk" -spc "D:\SSLCerts\SPSiteTestDev.cer" -pfx "D:\SSLCerts\SPSiteTestDev.pfx" -pi Password

SharePoint App-Only Policy

App-Only Policy

A user must be a site collection administrator to be able to grant use of the app-only policy. If the app-only policy is granted and the app already has tenant-scoped permissions, then the user must be a tenant administrator to be able to grant use of the app-only policy.  Only apps with web applications running outside of SharePoint can create and pass app-only tokens.

Once app-only policy is enabled, an app can choose to use an app-only policy or it can use the user + app policy token on each request. The italicized section of the following code example shows how an app can get an app + user policy access token.

string contextTokenString = TokenHelper.GetContextTokenFromRequest(Request);
if (contextTokenString != null)
{
     //Get context token.
     SharePointContextToken contextToken =
          TokenHelper.ReadAndValidateContextToken(contextTokenString, Request.Url.Authority);

     Uri sharepointUrl = new Uri(Request.QueryString["SPHostUrl"]);
            //Get App + User access token.
     string accessToken =
          TokenHelper.GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken;

      ClientContext clientContext =
           TokenHelper.GetClientContextWithAccessToken(sharepointUrl.ToString(), accessToken);

      //Do something. 
       ...

      //Get app only access token.
       string appOnlyAccessToken = TokenHelper.GetAppOnlyAccessToken(contextToken.TargetPrincipalName, sharepointUrl.Authority, contextToken.Realm).AccessToken;
      
      //Do something.
         ...
}

Add and Retrieve property bag by CSOM

This blog will tell you how to add and retrieve web property bag through SharePoint CSOM.

I tried use Add method to insert a new property; however, it does not really add the new property.

Add Property

private int SetProperty(ClientContext clientContext, int flag)
{
  Web web = clientContext.Site.RootWeb;
  /* Add successfully, but not persistantly. Cannot find this new property when retrieve property bag
  clientContext.Load(web, w=>web.AppProperties);
  clientContext.ExecuteQuery();

  if (!web.AllProperties.FieldValues.ContainsKey("Customized"))
  {
     web.AllProperties.FieldValues.Add("Customized", flag);
  }
  else
  {
     web.AllProperties["Customized"] = flag;
  }
  */ 
  
  // Correct Approach
  var allProperties = web.AllProperties;
  allProperties["CIBCCustomized"] = flag;

  web.Update();
  clientContext.ExecuteQuery();

  return flag;
}

Retrieve Property

private int GetFlag(ClientContext clientContext)
{
  Web web = clientContext.Site.RootWeb;
  clientContext.Load(web, w => w.AllProperties);
  clientContext.ExecuteQuery();

  if (!web.AllProperties.FieldValues.ContainsKey("Customized"))
  {
    return 0;
  }
  else
  {
    return (int)web.AllProperties["Customized"];
  }
}