Thursday, September 12, 2019

Use Office 365 connected groups in Yammer

Yammer platform allows you to integrate your Yammer groups to Office 365. Technically it means that each time when new Yammer group will be created it will be also created in Azure AD of the tenant of this Yammer network. More information about Yammer and O365 groups can be found in this article: Yammer and Office 365 Groups. This article also contains information how to enable Office 365 connected groups in Yammer and which conditions should be met before you will be able to enable it:

  • You must enforce Office 365 identity for Yammer users. When you first enforce Office 365 identity there is a seven-day trial period, after which the Status of your Office 365 Identity Enforcement changes to Committed.
  • Your Yammer network must be in a 1:1 network configuration. This means you have one Yammer network that is associated with one Office 365 tenant.

Note that currently it is possible to enforce Office 365 identity for Yammer users without 7 days trial i.e. right away.

In order to enable Office 365 connected groups in Yammer it you need to login to your Yammer network with administrator account and go to Network settings (gear icon in top left) > Network Admin > Security setting. On this page at first we need to enforce Office 365 identity for Yammer users:

Once it’s status will be changed to Committed after some time “Office 365 Connected Yammer Groups” setting will be changed to Enabled:

Documentation above says that it takes up to 24h to change status to Enabled. But in my case it was changed quite fast: almost immediately after enforcing of Office 365 identity for Yammer users.

Let’s see what happens when “Office 365 Connected Yammer Groups” setting is disabled for your Yammer network. When you create new Yammer group:

this group won’t be found in Azure AD of the Yammer network’s tenant:

After Office 365 Connected Yammer Groups have been enabled Yammer groups will appear in Yammer:

So you will be able to use all advantages of O365 connected groups. Note that for those Yammer groups which already exist when O365 connected groups had been enabled also appropriate O365 groups will be created after some time. Documentation says that it may take up to 1 week:

After about 1 week, existing eligible groups will be converted to Office 365 groups.

but for me it also happened quite fast within 1 hour or so.

Wednesday, September 11, 2019

Camlex 5.1.1 and Camlex.Client 3.2.1 are released

Today I’ve released new versions of Camlex library: Camlex 5.1.1 and Camlex.Client 3.2.1 (for Sharepoint client object model). In these versions reverse engineering support has been added for Includes/NotIncludes operations (support for Includes/NotIncludes operations was added in Camlex 5.1 and Camlex.Client 3.2). This is common practice currently that at first basic feature is released and then reverse engineering support is added into next minor release. Also http://camlex-online.org service was updated with new version.

Primary goal of reverse engineering is to power Camlex Online web site where developers may convert plain CAML query to C# code with Camlex. I.e. it simplifies usage of Camlex for developer who doesn’t familiar with its syntax yet (also there should not be those nowadays Smile ). I.e. you may enter CAML query like:

<Query>
  <Where>
      <Includes>
        <FieldRef Name="Title" />
        <Value Type="Text">Hello</Value>
      </Includes>
  </Where>
</Query>

and it will convert to to the following C# code:

Camlex.Query().Where(x => ((object)(string)x["Title"]).Includes((object)"Hello")) 

Tuesday, September 3, 2019

Problem with not unique web ids for Sharepoint Online web sites created with Fast Site Collection creation

When you create modern Team or Communication sites which use Fast Site Collection creation (not only from UI but also e.g. using New-PnPUnifiedGroup cmdlet) you may face with unexpected surprise: web id of the root sites (SPWeb.ID) of different site collections created this way may be equal!

Connect-PnPOnline https://{mytenant}.sharepoint.com/sites/site1
Get-PnPWeb
Title             ServerRelativeUrl        Id

-----             -----------------        --

site1             /sites/site1             {guid1}


Connect-PnPOnline https://{mytenant}.sharepoint.com/sites/site2
Get-PnPWeb
Title             ServerRelativeUrl        Id

-----             -----------------        --

site2             /sites/site2             {guid1}

This may be crucial if you have custom features which depend on web id. At the same time site collection ids appears to be different (SPSite.ID). Which means that if you have custom feature which identifies sites by SPWeb.ID only it will be safer to modify it so it will use pair (SPSite.ID, SPWeb.ID) to identify sites.

Friday, August 30, 2019

How to force ItemUpdating/ItemUpdated events in event receivers for all list items in Sharepoint list via PowerShell

If you need to force ItemUpdating/ItemUpdated events in all event receivers attached to Sharepoint list you may go through each item one by one, click Edit item and then click Save. However if there are many list items it is better to use PowerShell script which will force these events for all list items one by one:

param(
    [string]$url,
    [string]$listTitle
)

$web = Get-SPWeb $url
$list = $web.Lists[$listTitle]
foreach ($item in $list.Items)
{
    $item.Update()
    Start-Sleep -Seconds 5
}

In this script we iterate trough all list items in the list and call SPListItem.Update() method. In turn it forces attached event receivers to generate ItemUpdating and ItemUpdated events. After each update we wait 5 seconds to make sure event receivers finish previous event before to handle next one. If this is not needed for your scenario or if event receivers work faster you may comment or decrease number of seconds to wait.

Thursday, August 29, 2019

Enumerate all event receivers attached to Sharepoint list via PowerShell

Sometimes we need to enumerate all event receivers attached to specific Sharepoint list. The simplest way to do that is to use PowerShell. The following PowerShell script shows all event receivers attached to specified list:

param( 
    [string]$url,
    [string]$listName
)

$web = Get-SPWeb $url
$list = $web.Lists[$listName]

foreach($eventReceiverDef in $list.EventReceivers)
{
    $eventInfo = $eventReceiverDef.Class + ", " + $eventReceiverDef.Assembly + " – " + $eventReceiverDef.Type
    Write-Host $eventInfo -ForegroundColor green
}

In order to run it specify url of the web site which contains the list and list title:

check.ps1 http://example.com MyList

Here is example of running this script for standard Discussino board list:

Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdding
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemUpdating
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemDeleting
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemUpdated

Sunday, August 25, 2019

Internal mechanism of reply emails in Sharepoint discussion board

In Sharepoint you may create discussion board lists where users may create new discussion threads. When somebody writes reply into particular discussion author of this discussion receives email notification. In this post I will write about internal mechanism of these reply emails i.e. how they are implemented internally.

We may expect that reply notification emails in discussion boards are implemented via standard Sharepoint email alerts. However this is not the case. If you will check alerts list of parent web of discussion board list you will see that it will be empty (or it may contain alerts created in different list. Also it may contain alerts for discussion board but this is different story – I will write more about it below):

$web = Get-SPWeb http://example.com
$web.Alerts

I.e. authors of discussion board will still get email notifications on replies even when there are no alerts in the web. It mean that these reply emails are implemented via some different mechanism. Also it means that it is not possible to edit template of these emails by modifying Sharepoint alert templates. Users may still use OTB Sharepoint alerts and subscribe themselves to events in discussion board list: by clicking three dots near discussion thread subject and selecting Alert me link:

In this case real alert will be created and web.Alerts collection will contain it. This alert will be customizable i.e. it will be possible to modify it’s template by editing discussion board alert template. However still it will be different alert from reply notification email: if author of discussion will subscribe him or herself on discussion board event this way then author will get 2 alerts – one as reply notification and another as OTB alert.

So how reply email notifications are implemented then? If it is not OTB alert it may be implemented rather via workflow or via event receiver. If we will check list of workflows for discussion board we will see that it is empty. So only event receivers remain. Let’s try to execute the following PowerShell script which lists all event receivers for specified list:

param( 
    [string]$url,
    [string]$listName
)

$web = Get-SPWeb $url
$list = $web.Lists[$listName]

foreach($eventReceiverDef in $list.EventReceivers)
{
    $eventInfo = $eventReceiverDef.Class + ", " + $eventReceiverDef.Assembly + " – " + $eventReceiverDef.Type
    Write-Host $eventInfo -ForegroundColor green
}

For discussion board it will show the following event receivers:

Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdding
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemUpdating
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemDeleting
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.Portal.CommunityEventReceiver, Microsoft.SharePoint.Portal, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c - ItemAdded
Microsoft.SharePoint.DiscussionListEventReceiver, Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c - ItemUpdated

DiscussionListEventReceiver basically updates LastReplyBy field of the parent discussion board and doesn’t do other actions. So let’s check CommunityEventReceiver. If we will check it’s code via decompiler we will see that in all post event handler methods (ItemAdded, ItemUpdated, ItemDeleted) it calls internal method HandleEvent which in turn calls EventCache.Instance.HandleChange() method:

public sealed class CommunityEventReceiver : SPItemEventReceiver
{
 public CommunityEventReceiver()
 {
 }

 private void HandleEvent(SPItemEventProperties properties)
 {
  bool eventFiringEnabled = base.EventFiringEnabled;
  try
  {
   base.EventFiringEnabled = false;
   using (SPMonitoredScope sPMonitoredScope = new SPMonitoredScope("CommunityEventReceiver::HandleEvent"))
   {
    EventChangeRecord eventChangeRecord = null;
    SPSecurity.RunWithElevatedPrivileges(() => {
     using (SPSite sPSite = new SPSite(properties.Web.Site.ID))
     {
      using (SPWeb sPWeb = sPSite.OpenWeb(properties.Web.ID))
      {
       eventChangeRecord = EventCache.Instance.HandleChange(sPWeb, properties);
      }
     }
    });
    if (eventChangeRecord != null && eventChangeRecord.SocialPostCreationData != null)
    {
     FeedNotificationUtils.AddSocialPostNotification(properties.Web, eventChangeRecord.SocialPostCreationData);
    }
   }
  }
  finally
  {
   base.EventFiringEnabled = eventFiringEnabled;
  }
 }

 public override void ItemAdded(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 public override void ItemDeleted(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 public override void ItemUpdated(SPItemEventProperties properties)
 {
  this.HandleEvent(properties);
 }

 ...
}

(There are also pre events ItemAdding, ItemUpdating, ItemDeleting but they are not relevant to this post). Let’s now see what happens inside EventCache.Instance.HandleChange. Among with other actions it iterates through internal handlers collection and calls HandleEvent method for each handler in this collection:

public EventChangeRecord HandleChange(SPWeb web, SPItemEventProperties properties)
{
 ...
  BaseCommunityEventHandler[] baseCommunityEventHandlerArray = this.handlers;
  for (int i = 0; i < (int)baseCommunityEventHandlerArray.Length; i++)
  {
   BaseCommunityEventHandler baseCommunityEventHandler = baseCommunityEventHandlerArray[i];
   if (baseCommunityEventHandler.HandledTemplateType == (int)list.BaseTemplate || baseCommunityEventHandler.HandledTemplateType == BaseCommunityEventHandler.HandleAllTemplateTypes)
   {
    baseCommunityEventHandler.HandleEvent(properties, eventChangeRecord);
   }
  }
  ...
 }
 ...
}

Now let’s see what exact handlers are added to this collection:

private void InitializeHandlers()
{
 BaseCommunityEventHandler[] discussionListCommunityEventHandler = new BaseCommunityEventHandler[] { new DiscussionListCommunityEventHandler(), new CategoriesListCommunityEventHandler(), new ReputationCommunityEventHandler(), new MembersListCommunityEventHandler(), new BadgesListCommunityEventHandler(), new CommunityNotificationsEventHandler() };
 this.handlers = discussionListCommunityEventHandler;
}

So there are quite many handlers which implement different features of community sites (reputations, membership, bages, etc). One of them is CommunityNotificationsEventHandler – this is exact handler which sends email notification on reply from discussion board:

internal class CommunityNotificationsEventHandler : BaseCommunityEventHandler
{
 internal override void HandleEvent(SPItemEventProperties properties, EventChangeRecord record)
 {
  ...
  SPList list = record.GetList("properties");
  if (record.EventType == SPEventReceiverType.ItemAdded)
  {
    FeedNotificationUtils.SendEmailNotificationOnReply(record.Web, sPListItem, listItem);
  }
  ...
 }
}

It calls internal FeedNotificationUtils.SendEmailNotificationOnReply() method which sends actual email:

internal static class FeedNotificationUtils
{
 ...
 public static bool SendEmailNotificationOnReply(SPWeb communityWeb, SPListItem topic, SPListItem reply)
 {
  bool flag = false;
  try
  {
   SPUser author = FeedNotificationUtils.GetAuthor(communityWeb, topic);
   if (FeedNotificationUtils.ShouldSendReplyNotification(communityWeb, reply, author))
   {
    UserProfile userProfile = CommonFeedNotificationUtils.GetUserProfile(communityWeb, author);
    if (userProfile != null && (userProfile.get_EmailOptin() & 64) == 0)
    {
     UserProfileApplicationProxy proxy = UserProfileApplicationProxy.GetProxy(CommonFeedNotificationUtils.GetServiceContext(communityWeb));
     string mySitePortalUrl = proxy.GetMySitePortalUrl(ServerApplication.get_CurrentUrlZone(), userProfile.get_PartitionID());
     using (SPSite sPSite = new SPSite(mySitePortalUrl))
     {
      MailMessage mailMessage = null;
      try
      {
       string mySiteEmailSenderName = proxy.GetMySiteEmailSenderName(userProfile.get_PartitionID());
       mailMessage = FeedNotificationUtils.CreateReplyNotificationMailMessage(sPSite.RootWeb, mySiteEmailSenderName, communityWeb, author, topic, reply, mySitePortalUrl);
       using (SPSmtpClient sPSmtpClient = new SPSmtpClient(communityWeb.Site))
       {
        flag = SPMailMessageHelper.TrySendMailMessage(sPSmtpClient, mailMessage);
        if (!flag)
        {
         ...
        }
       }
      }
      finally
      {
       if (mailMessage != null)
       {
        SPMailMessageHelper.DisposeAttachmentStreams(mailMessage);
        mailMessage.Dispose();
       }
      }
     }
    }
   }
  }
  catch (Exception exception1)
  {
   ...
  }
  return flag;
 }
}

So as you can see reply emails in discussion boards are implemented via event handlers. At the end let’s also mention that it is possible to disable reply emails in discussion board – but it will also disable other community features like ratings (i.e. users won’t be able to like replies). In order to do that go to discussion board list settings > Rating settings and set “Allow items in this list to be rated” to No:

It will call internal method ReputationHelper.DisableReputation() which will remove CommunityEventReceiver from discussion board:

internal static class ReputationHelper
{
 ...
 internal static void DisableReputation(SPList list)
 {
  ReputationHelper.HideAllReputationFields(list);
  ReputationHelper.SetExperience(list, string.Empty, false);
  if (list.BaseTemplate == SPListTemplateType.DiscussionBoard)
  {
   List sPViews = new List();
   foreach (SPView view in list.Views)
   {
    sPViews.Add(view);
   }
   Guid[] contentReputationPopularityFieldId = new Guid[] { CommunitiesConstants.ContentReputation_Popularity_FieldId, CommunitiesConstants.ContentReputation_DescendantLikesCount_FieldId, CommunitiesConstants.ContentReputation_DescendantRatingsCount_FieldId, CommunitiesConstants.ContentReputation_LastRatedOrLikedBy_FieldId };
   FunctionalityEnablers.RemoveFieldsFromViews(contentReputationPopularityFieldId, list, sPViews);
   CommunityUtils.RemoveEventReceiver(list, typeof(CommunityEventReceiver).FullName);
   foreach (SPView sPView in sPViews)
   {
    if (sPView.JSLink != null)
    {
     sPView.JSLink = sPView.JSLink.Replace("|sp.ui.communities.js", "");
    }
    sPView.Update();
   }
  }
 }
}

This is how email notifications work in Sharepoint discussion boards. Hope that this information will help you in your work.

Thursday, July 18, 2019

Problem with Copy-PnPFile cmdlet and File Not Found error

Sharepoint PnP PowerShell library has many useful commands which simplify scripting against Sharepoint Online. One of them is Copy-PnPFile which allows to copy file from one document library to another. Source and target doclibs may be even located in different site collections in the same tenant.

One of example of Copy-PnPFile (basically 1st example) says that it is possible to copy single file like that:

Copy-PnPFile -SourceUrl Documents/company.docx -TargetUrl /sites/otherproject/Documents/company.docx

Unfortunately currently it gives File not found error. Looks like there is a bug in Copy-PnPFile which prevents it from working correctly – it is also described in this StackOverflow thread which was created couple of days ago: Copy-PnPFile returns File Not Found.

The only working way which I’ve found so far is to copy whole root folder:

Copy-PnPFile -SourceUrl Documents -TargetUrl /sites/otherproject/Documents -SkipSourceFolderName

It will copy all files from Documents doclib to Documents doclib on another site collection /sites/otherproject. However it will also try to copy OTB list view AllItems.aspx as last file and it will give error that AllItems.aspx already exists in target doclib. In order to ignore this error I used the following solution:

$error = $null
Copy-PnPFile -SourceUrl Documents -TargetUrl /sites/otherproject/Documents -SkipSourceFolderName -ErrorAction SilentlyContinue -ErrorVariable error
if ($error -and !$error.Exception.Message.ToLower().Contains("allitems.aspx")) {
    throw $error
}

I.e. it will throw error only if message doesn’t contain allitems.aspx occurrence. Hope that it will help someone.