Showing posts with label Sharepoint online. Show all posts
Showing posts with label Sharepoint online. Show all posts

Tuesday, August 8, 2023

Camlex and Camlex.Client 5.4.2 released

New version 5.4.2 of Camlex library has been released. Starting with this version it became possible to generate CAML queries with string operators BeginsWith and Contains for ContentTypeId field type e.g. the following C# code:

Camlex.Query().Where(x => ((DataTypes.ContentTypeId)x["ContentTypeId"]).StartsWith("0x123")).ToString(true);

will generate the following CAML query:

<Query>
  <Where>
    <BeginsWith>
      <FieldRef Name="ContentTypeId" />
      <Value Type="ContentTypeId">0x123</Value>
    </BeginsWith>
  </Where>
</Query>

This is useful since when you add some site content type to SharePoint list under the hood SharePoint creates inherited content type (which has ContentTypeId which starts with ContentTypeId of parent site content type with appended 00 symbols and guid without dashes) and exactly this inherited content type is then used for list items created in this list. In order to fetch all items created with original site content type we may use CAML query with BeginsWith operator and ContentTypeIdof parent site content type.

As usual new version is available via Nuget.

Wednesday, November 16, 2022

Camlex and Camlex.Client 5.4.1 released

I'm glad to announce that today new version 5.4.1 of Camlex/Camlex.Client libraries were released. This is minor release which contains fix for reverse engineering of binary operations (Geq, Gt, Leq, Lt) for text values. Reverse engineering is used in free online service http://camlex-online.org where Sharepoint developers which are new to Camlex can automatically convert classic CAML query to C# syntax for Camlex. Thus it will simplify migration of existing code to Camlex.

Credits for this release go to Ivan Russo which contributed to Camlex (thanks a lot Ivan). If you are Sharepoint developer and have idea how to improve Camlex feel free to create PR :).

Wednesday, June 8, 2022

Change url of Sharepoint Online list or document library via PnP.PowerShell

In order to change url of Sharepoint Online list or document library you may use the following PnP.PowerShell script:

Connect-PnPOnline -Url https://{tenant}.sharepoint.com/sites/foo
$list = Get-PnPList MyList
$list.RootFolder.MoveTo("MyListNewUrl")
$ctx = Get-PnPContext
$ctx.ExecuteQuery()

In this example we change list url to MyListNewUrl.

Friday, May 6, 2022

Export Sharepoint Online lists with their content to PnP template via PnP PowerShell

As you probably know it is possible to export Sharepoint Online sites to PnP template using Get-PnPSiteTemplate cmdlet. By default it will export only structure but it is also possible to export Sharepoint lists with content (list items) to PnP template. This is quite commonly needed request in many maintenance tasks. Of course you may save list as template with content from UI but if you need to automate it this option is not very convenient.

If you need to export Sharepoint list with its content to PnP template use the following commands:

Connect-PnPOnline -Url https://{mytenant}.sharepoint.com/sites/foo -Interactive
Get-PnPSiteTemplate -Out template.pnp -ListsToExtract "Test" -Handlers Lists
Add-PnPDataRowsToSiteTemplate -Path template.pnp -List "Test"

In this example we export list Test with list items to PnP template template.pnp. Hope that it will help someone.

Wednesday, April 13, 2022

Resolve “Everyone except external users” group using PnP.PowerShell

In my previous posts I showed several ways to resolve special group in Sharepoint Online "Everyone except external users" which represents all users in organization except external users:

In this post I will show how to do that with PnP.PowerShell. Simplest way which will work on most tenants is the following:

$authRealm = Get-PnPAuthenticationRealm
$everyOneExceptExternals = Get-PnPUser -Id "c:0-.f|rolemanager|spo-grid-all-users/$authRealm"

But on some tenants (e.g. old tenants) it may not work because this special group was created with different naming convention there (see link above). For such tenants we may use the following additional step:

if (!$everyOneExceptExternals) {
	$everyOneExceptExternals = Get-PnPUser | Where-Object { $_.LoginName.StartsWith("c:0-.f|rolemanager|spo-grid-all-users/") }
}

Here we try to find user which login name starts with special "c:0-.f|rolemanager|spo-grid-all-users/" prefix. This prefix is used in login name of "Everyone except external users" group. With this approach you may resolve this special group both on new and old tenants. Hope it will help someone.

Friday, April 8, 2022

Create folders with special characters in Sharepoint Online programmatically via CSOM

If you worked with SP on-prem you probably know that some special characters are not allowed in folders names there. In Sharepoint Online however it is possible to use some special characters in folders names:

How to create such folders with special characters programmatically via CSOM? If we will try to do it using the same approach which we used for SP on-prem it will implicitly remove all parts in folder names which come after special characters i.e. in above example abc, def, ghi:

public static ListItem AddFolder(ClientContext ctx, List list, string parentFolderUrl, string folderName)
{
    var lici = new ListItemCreationInformation
    {
        UnderlyingObjectType = FileSystemObjectType.Folder,
        LeafName = folderName.Trim(),
        FolderUrl = parentFolderUrl
    };

    var folder = list.AddItem(lici);
    folder["Title"] = lici.LeafName;
    folder.Update();
    ctx.ExecuteQueryRetry();

    return folder;
}

If we want to create folders with special characters we need to use another CSOM method Folder.AddSubFolderUsingPath:

public static void AddFolderUsingPath(ClientContext ctx, Folder parentFolder, string folderName)
{
    parentFolder.AddSubFolderUsingPath(ResourcePath.FromDecodedUrl(folderName));
    ctx.ExecuteQueryRetry();
}

With this method folders with special characters will be created successfully in Sharepoint Online.

Thursday, March 24, 2022

How to identify Sharepoint Online sites which belongs to Teams private channels

In MS Teams team owner may create private channels: only members of these channels will have access to these channels. What happens under the hood is that for each private channel Teams creates separate SPO site collection with own permissions. E.g. if we have team with 2 private channels channel1 and channel2:

it will create 2 SPO sites with the following titles:

  • {team name} - channel1
  • {team name} - channel2

If we will visit these sites in browser we will notice that there will be teams icon near site title and "Private channel | Internal" site classification label:


How we may identify such SPO sites which correspond to teams private channels? E.g. if want to fetch all such sites via search.

At first I tried to check web property bag of these sites because this is how we may identify that site belongs to O365 group (see Fetch Sharepoint Online sites associated with O365 groups via Sharepoint Search KQL) but didn't find anything there. The I used Sharepoint Search Query Tool and found that these sites have specific WebTemplate = TEAMCHANNEL:

So in order to identify SPO sites which correspond to teams private channels we may use the following KQL:

WebTemplate:TEAMCHANNEL

It will return all sites for teams private channels.

Tuesday, March 1, 2022

Return image stored in Sharepoint Online doclib from Azure function and show it in SPFx web part

Imagine that we need to display image which is stored e.g. in Style library doclib of Sharepoint Online site collection (SiteA) on another site collection (SiteB) and that users from SiteB may not have permissions on SiteA. One solution is to return this image in binary form from Azure function (which in turn will read it via CSOM and app permissions) and display in SPFx web part in base64 format. With this approach way we can avoid SPO permissions limitation (assuming that Azure functions are secured via AAD: Call Azure AD secured Azure functions from C#).

At first we need to implement http-triggered Azure function (C#) which will return requested image (we will send image url in query string param). It may look like this (for simplicity I removed errors handling):

[FunctionName("GetImage")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)]HttpRequestMessage req, TraceWriter log)
{
	string url = req.GetQueryNameValuePairs().FirstOrDefault(q => string.Compare(q.Key, "url", true) == 0).Value;
	var bytes = ImageHelper.GetImage(url);
	
	var content = new StreamContent(new MemoryStream(bytes));
	content.Headers.ContentType = new MediaTypeHeaderValue(ImageHelper.GetMediaTypeByFileUrl(url));

	return new HttpResponseMessage(HttpStatusCode.OK)
	{
		Content = content
	};
}

Here 2 helper functions are used: one which gets actual image as bytes array and another which returns media type based on file extension:

public static class ImageHelper
{
	public static byte[] GetImage(string url)
	{
		using (var ctx = ...) // get ClientContext
		{
			var web = ctx.Web;
			var file = web.GetFileByServerRelativeUrl(new Uri(url).AbsolutePath);
			ctx.Load(file);
			
			var fileStream = file.OpenBinaryStream();
			ctx.ExecuteQuery();
			
			byte[] bytes = new byte[fileStream.Value.Length];
			fileStream.Value.Read(bytes, 0, (int)fileStream.Value.Length);
			return bytes;
		}
	}

	public static string GetMediaTypeByFileUrl(string url)
	{
		string ext = url.Substring(url.LastIndexOf(".") + 1);
		switch (ext.ToLower())
		{
			case "jpg":
				return "image/jpeg";
			case "png":
				return "image/png";
			... // enum all supported media types here
			default:
				return string.Empty;
		}
	}
}

Now we can call this AF from SPFx:

return await this.httpClient.get(url, SPHttpClient.configurations.v1,
	{
		headers: ..., // add necessary headers to request
		method: "get"
	}
).then(async (result: SPHttpClientResponse) => {
	if (result.ok) {
		let binaryResult = await result.arrayBuffer();
		return new Promise((resolve) => {
			resolve({ data: binaryResult, type: result.headers.get("Content-Type") });
	});
})

And the last step is to encode it to base64 and add to img src attribute:

let img = ...; // get image element from DOM
let result = await getImage(url);
img.setAttribute("src", `data:${result.type};base64,${Buffer.from(result.data, "binary").toString("base64")}`)

After that image from SiteA will be shown on SiteB even if users don't have access to SiteA directly.

Wednesday, February 9, 2022

Problem with SPO app bar and Teams custom app with static tabs

If you use staticTabs in your custom MS Teams app (see Manifest schema for Microsoft Teams):

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
  "manifestVersion": "1.5",
  ...
  "staticTabs": [
    {
      "entityId": "Foo",
      "name": "Bar",
      "contentUrl": "...",
      "websiteUrl": "https://example.com",
      "scopes": [
        "personal"
      ]
    }
  ],
  ...
}

you may face with the following issue: when user clicks on app icon it correctly opens web page defined in staticsTabs. But if user clicks on that second time after web page has been loaded then SPO app bar callout will be shown with My sites/My news.

In order to fix it the following workaround can be used: identify that app on opened web page is running inside Teams (this article contains details how to do that: How to identify whether SPFx web part is running in web browser or in Teams client) and hide SPO app bar callouts via css in this case. This is how it can be done via TypeScript (for this example we assume that app is SPFx web part running on SPO page):

if (isAppRunningInsideTeams()) {
  const style = document.createElement("style");
  style.textContent = "#sp-appBar-callout { display:none !important; } ";
  const head = document.getElementsByTagName("head")[0];
  head.appendChild(style);
}

It will hide callouts only inside Teams where this problem happens and at the same time it will still work in SPO.

Monday, January 31, 2022

How to get all items from large lists in Sharepoint Online

As you probably know Sharepoint lists have default throttling limit 5000 items which means that if query result to more than 5000 items it will throw resource throttling exception. One solution which you may try is to use RowLimit:

<View>
    <Query>
        <Where>
            <Eq>
                <FieldRef Name=\"Title\" />
                <Value Type=\"Text\">test</Value>
            </Eq>
        </Where>
    </Query>
    <RowLimit>5000</RowLimit>
</View>

Or with Camlex:

var camlQuery = Camlex.Query()
    .Where(x => (string)x["Title"] == "test")
    .Take(5000)
    .ToCamlQuery();

However if list contains more than 5000 items this query will anyway result in resource throttling exception regardless of RowLimit being set. Another possible solution is to try to add index to set some field as indexed and use condition against this field in query. But what if you can't do that (e.g. if you don't have control over the list schema)? How to get all items from large list then?

For that you may use query ID > 0 (which is always true since ID is auto generated integer) and CamlQuery.ListItemCollectionPosition property which allows to get paginated results. Code will look like this:

ListItemCollectionPosition position = null;
var items = new List<ListItem>();
do
{
    var camlQuery = Camlex.Query()
        .Where(x => (int)x["ID"] > 0)
        .ViewFields(x => new []
        {
            x["ID"],
            x["Title"]
        })
        .Take(5000)
        .ToCamlQuery();
    camlQuery.ListItemCollectionPosition = position;
    var listItems = list.GetItems(camlQuery);
    ctx.Load(listItems);
    ctx.ExecuteQueryRetry();

    items.AddRange(listItems);
    position = listItems.ListItemCollectionPosition;

    if (position == null)
    {
        break;
    }
} while (true);

It will fetch items by 5000 items per page until all items from large list will be fetched.

Thursday, December 16, 2021

Camlex 5.4 release: switch to MIT license

Hello Sharepoint developers who use Camlex in the work. I'm glad to announce that starting with version 5.4 Camlex will use MIT license. Before that Camlex was distributed under Ms-Pl license but nowdays MIT became standard for open source projects as most permissive license (e.g. PnP.Framework also uses MIT license). In order to be inline with the trend I changed Camlex license to MIT. New nuget packages with 5.4 version for basic object model version and CSOM version are already available for download.

Thursday, November 25, 2021

Sharepoint Online remote event receivers attached with Add-PnPEventReceiver depend on used authentication method

Today I have faced with another strange problem related with SPO remote event receivers (wrote about another strange problem here: Strange problem with remote event receivers not firing in Sharepoint Online sites which urls/titles ends with digits). I attach remote event receiver using Add-PnPEventReceiver cmdlet like that:

Connect-PnPOnline -Url ...
$list = Get-PnPList "MyList"
Add-PnPEventReceiver -List $list.Id -Name TestEventReceiver -Url ... -EventReceiverType ItemUpdated -Synchronization Synchronous

In most cases RER is attached without any errors. But the question will it be fired after that. As it turned out it depends how exactly you connect to the parent SPO site with Connect-PnPOnline: remote event receiver is attached successfully in all cases (you may see them in SPO client browser) but in some cases they won't be triggered. In fact I found that they are triggered only if you connect with "UseWebLogin" parameter, while in all other cases they are not. In the below table I summarized all methods which I tried:

# Method Is RER fired?
1
Connect-PnPOnline -Url ... -ClientId {clientId}-Interactive
No
2 Connect-PnPOnline -Url ... -ClientId {clientId}     No
3 Connect-PnPOnline -Url ...
No
4 Connect-PnPOnline -Url ... -UseWebLogin
Yes


Monday, November 22, 2021

Strange problem with remote event receivers not firing in Sharepoint Online sites which urls/titles ends with digits

Some time ago I wrote about Sharepoint Online remote event receivers (RERs) and how to use Azure functions with them: Use Azure function as remote event receiver for Sharepoint Online list and debug it locally with ngrok. When I tested RERs on another SPO sites I've faced with very strange problem: on those sites which urls/titles end with digits remote event receivers were not fired. E.g. I often create modern Team/Communication sites with datetime stamp at the end:

https://{tenant}.sharepoint.com/sites/{Prefix}{yyyyMMddHHmm}
e.g.
https://{tenant}.sharepoint.com/sites/Test202111221800

I noticed that on such sites RER was not called because of some reason. I've used the same PowerShell script for attaching RER to SPO site as described in above article and the same Azure function app running locally with the same ports and ngrok tunneling.

After I created site without digits at the end (https://{tenant}.sharepoint.com/sites/test) - RER started to work (without restarting AF or ngrok - i.e. I used the same running instances of AF/ngrok for all tests which ran all the time). Didn't find any explanation of this problem so far. If you faced with this issue and know the reason please share it.

Monday, October 11, 2021

One reason for Connect-SPOService error: The sign-in name or password does not match one in the Microsoft account system

Today we have faced with interesting issue. When tried to use Connect-SPOService cmdlet on one tenant we got the following error:

The sign-in name or password does not match one in the Microsoft account system

Interesting that on other tenants this cmdlet worked properly. Troubleshooting and searching showed that one of the reason of this error can be enabled MFA (more specifically, when Connect-SPOService is used with -Credentials param). However we double checked that for those accounts for which this error was shown MFA was disabled.

Then we tried to login to O365 site with this account in browser and my attention was attracted by the following message which was shown after successful login:

Microsoft has enabled security defaults to keep your account secure:

 


As it turned out on this tenant AAD Security defaults were enabled which forced MFA for all users. In turn it caused mentioned error with Connect-SPOService. Solution was to disable security defaults in AAD properties:

After that error disappeared and we were able to use SPO cmdlets.

Thursday, September 30, 2021

Speaking on online IT community event

Today I was speaking on online IT community event about development and debugging of Azure functions: https://tulaitcommunity.ru. On this event I wanted to show how to efficiently develop Azure functions and use them in Sharepoint Online. Development of Azure functions for Sharepoint Online require skills and experience of usage of many tools like Visual Studio, Postman, ngrok, PnP PowerShell, SPO browser. They were used in my demo. Presentation from speech is available from Slideshare here: https://www.slideshare.net/sadomovalex/azure-sharepoint-online. Recording from online meeting is available on youtube (on Russian language):


 

Hope that it was interesting :)

Thursday, July 22, 2021

Use Azure function as remote event receiver for Sharepoint Online list and debug it locally with ngrok

If you worked with Sharepoint on-prem you probably know what are event receivers: custom handlers which you may subscribe on different types of events. They are available for different levels (web, list, etc). In this article we will talk about list event receivers.

In Sharepoint Online we can't use old event receivers because they should be installed as farm solutions which are not available in SPO. Instead we have to use remote event receivers. The concept is very similar but instead of class and assembly names we should provide end point url where SPO will send HTTP POST request when event will happen.

Let's see how it works on practice. For event receiver end point I will use Azure function and will run it locally. For debugging it I will use ngrok tunneling (btw ngrok is great service which makes developers life much easier. If you are not familiar with it yet I hardly suggest you to do that :) ). But let's go step by step.

First of all we need to implement our Azure function:

[FunctionName("ItemUpdated")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
    log.Info("Start ItemUpdated.Run");
    var request = req.Content.ReadAsStringAsync().Result;
    return req.CreateResponse(HttpStatusCode.OK);
}

It doesn't do anything except reading body payload as string - we will examine it later. Then we run it locally - by default it will use http://localhost:7071/api/ItemUpdated url.

Next step is to create ngrok tunnel so we will get public https end point which can be used by SPO. It is done by the following command:

ngrok http -host-header=localhost 7071

After this command you should see something like that:

Now everything is ready for attaching remote event receiver to our list. It can be done by using Add-PnPEventReceiver cmdlet from PnP.Powershell. Note however that beforehand is it important to connect to target site with Connect-PnPOnline with UseWebLogin parameter:

Connect-PnPOnline -Url https://{tenant}.sharepoint.com/sites/{url} -UseWebLogin

Without UseWebLogin remote event receiver won't be triggered. Here is the issue on github which explains why: RemoteEventReceivers are not fired when added via PnP.Powershell.

When ngrok is running we need to copy forwarding url: those which uses https and looks like https://{randomId}.ngrok.io (see image above). We will use this url when will attach event receiver to target list:

Connect-PnPOnline -Url https://mytenant.sharepoint.com/sites/Test -UseWebLogin
$list = Get-PnPList TestList
Add-PnPEventReceiver -List $list.Id -Name TestEventReceiver -Url https://{...}.ngrok.io/api/ItemUpdated -EventReceiverType ItemUpdated -Synchronization Synchronous

Here I attached remote event receiver to TestList on site Test and subscribed it to ItemUpdated event. For end point I specified url of our Azure function using ngrok host. Also I created it as synchronous event receiver so it will be triggered immediately when item got updated in the target list. If everything went Ok you should see your event receiver attached to the target list using Sharepoint Online Client Browser:

Note that ReceiverUrl property will contain ngrok end point url which we passed to Add-PnPEventReceiver.

Now all pieces are set and we may test our remote event receiver: go to Test list and try to edit list item there. After saving changes event receiver should be triggered immediately. If you will check body payload you will see that it contains information about properties which have been changed and id of list item which we just modified:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
	<s:Body>
		<ProcessEvent xmlns="http://schemas.microsoft.com/sharepoint/remoteapp/">
			<properties xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
				<AppEventProperties i:nil="true"/>
				<ContextToken/>
				<CorrelationId>c462dd9f-604a-2000-ea96-f4bacc80aa84</CorrelationId>
				<CultureLCID>1033</CultureLCID>
				<EntityInstanceEventProperties i:nil="true"/>
				<ErrorCode/>
				<ErrorMessage/>
				<EventType>ItemUpdated</EventType>
				<ItemEventProperties>
					<AfterProperties xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
						<a:KeyValueOfstringanyType>
							<a:Key>TimesInUTC</a:Key>
							<a:Value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">TRUE</a:Value>
						</a:KeyValueOfstringanyType>
						<a:KeyValueOfstringanyType>
							<a:Key>Title</a:Key>
							<a:Value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">item01</a:Value>
						</a:KeyValueOfstringanyType>
						<a:KeyValueOfstringanyType>
							<a:Key>ContentTypeId</a:Key>
							<a:Value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">...</a:Value>
						</a:KeyValueOfstringanyType>
					</AfterProperties>
					<AfterUrl i:nil="true"/>
					<BeforeProperties xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays"/>
					<BeforeUrl/>
					<CurrentUserId>6</CurrentUserId>
					<ExternalNotificationMessage i:nil="true"/>
					<IsBackgroundSave>false</IsBackgroundSave>
					<ListId>96c8d1c1-de22-47bf-9f70-aeeefa349856</ListId>
					<ListItemId>1</ListItemId>
					<ListTitle>TestList</ListTitle>
					<UserDisplayName>...</UserDisplayName>
					<UserLoginName>...</UserLoginName>
					<Versionless>false</Versionless>
					<WebUrl>https://mytenant.sharepoint.com/sites/Test</WebUrl>
				</ItemEventProperties>
				<ListEventProperties i:nil="true"/>
				<SecurityEventProperties i:nil="true"/>
				<UICultureLCID>1033</UICultureLCID>
				<WebEventProperties i:nil="true"/>
			</properties>
		</ProcessEvent>
	</s:Body>
</s:Envelope>

This technique allows to use Azure function as remote event receiver and debug it locally. Hope it will help someone.

Update 2021-11-22: see also one strange problem about using of remote event receivers in SPO sites: Strange problem with remote event receivers not firing in Sharepoint Online sites which urls/titles ends with digits.

Monday, July 19, 2021

How to edit properties of SPFx web part using PnP.Framework

In my previous post I showed how to add SPFx web part on modern page using PnP.Framework (see How to add SPFx web part on modern page using PnP.Framework). In this post I will continue to familiarize readers of my blog with this topic and will show how to edit web part properties of SPFx web part using PnP.Framework.

For editing web part property we need to know 3 things:

  • web part id
  • property name
  • property value

You may get web part id and property name from manifest of your web part. Having these values you may set SPFx web part property using the following code:

var ctx = ...;
var page = ctx.Web.LoadClientSidePage(pageName);

IPageWebPart webpart = null;
foreach (var control in page.Controls)
{
    if (control is IPageWebPart && (control as IPageWebPart).WebPartId == webPartId)
    {
        webpart = control as IPageWebPart;
        break;
    }
}

if (webpart != null)
{
    var propertiesObj = JsonConvert.DeserializeObject<JObject>(webpart.PropertiesJson);
    propertiesObj[propertyName] = propertyValue;
    webpart.PropertiesJson = propertiesObj.ToString();
    page.Save();
    page.Publish();
}

At first we get instance of modern page. Then find needed SPFx web part on the page using web part id. For found web part we deserialize its PropertiesJson property to JObject and set its property to passed value. After that we serialized it back to string and store to webPart.PropertiesJson property. Finally we save and publish parent page. After that our SPFx web part will have new property set.

Wednesday, July 14, 2021

How to add SPFx web part on modern page using PnP.Framework

If you migrated from OfficeDevPnP (SharePointPnPCoreOnline nuget package) to PnP.Framework you will need to rewrite code which adds SPFx web parts on modern pages. Old code which used OfficeDevPnP looked like that:

var page = ctx.Web.LoadClientSidePage(pageName);

var groupInfoComponent = new ClientSideComponent();
groupInfoComponent.Id = webPartId;

groupInfoComponent.Manifest = webPartManifest;
page.AddSection(CanvasSectionTemplate.OneColumn, 1);

var groupInfiWP  = new ClientSideWebPart(groupInfoComponent);
page.AddControl(groupInfiWP, page.Sections[page.Sections.Count - 1].Columns[0]);

page.Save();
page.Publish();

But it won't compile with PnP.Framework because ClientSideWebPart became IPageWebPart. Also ClientSideComponent means something different and doesn't have the same properties. In order to add SPFx web part to the modern page with PnP.Framework the following code can be used:

var page = ctx.Web.LoadClientSidePage(pageName);

var groupInfoComponent = page.AvailablePageComponents().FirstOrDefault(c => string.Compare(c.Id, webPartId, true) == 0);
var groupInfoWP = page.NewWebPart(groupInfoComponent);
page.AddSection(CanvasSectionTemplate.OneColumn, 1);
page.AddControl(groupInfoWP, page.Sections[page.Sections.Count - 1].Columns[0]);

page.Save();
page.Publish();

Here we first get web part reference from page.AvailablePageComponents() and then create new web part using page.NewWebPart() method call. After that we add web part on a page using page.AddControl() as before. Hope it will help someone.

Tuesday, May 25, 2021

Camlex 5.3 and Camlex.Client 4.2 released: support for StorageTZ attribute for DateTime values

Today new versions of Camlex library (both for server side and CSOM) have been released: Camlex 5.3 and Camlex.Client 4.2. For client object model separate packages are available for Sharepoint online and on-premise:

Package Version Description
Camlex.NET.dll 5.3.0 Server object model (on-prem)
Camlex.Client.dll 4.2.0 Client object model (SP online)
Camlex.Client.2013 4.2.0 Client object model (SP 2013 on-prem)
Camlex.Client.2016 4.2.0 Client object model (SP 2016 on-prem)
Camlex.Client.2019 4.2.0 Client object model (SP 2019 on-prem)

In this release possibility to specify StorageTZ attribute for DateTime values was added. Main credits for this release go to Ivan Russo who implemented basic part of the new feature. So now when you create CAML query for DateTime values you may pass "true" into IncludeTimeValue() method which then will add StorageTZ="True" attribute for Value tag:

var now = new DateTime(2021, 5, 18, 17, 31, 18);
string caml = Camlex.Query().Where(x => (DateTime)x["Created"] > now.IncludeTimeValue(true)).ToString();

will generate the following CAML:

<Where>
  <Gt>
      <FieldRef Name="Created" />
      <Value Type="DateTime" IncludeTimeValue="True" StorageTZ="True">2021-05-18T17:31:18Z</Value>
  </Gt>
</Where>

Thank you for using Camlex and as usual, if you have idea for its further improvement you may post it here.

Thursday, May 20, 2021

One problem with bundling SPFx solution

If you run "gulp bundle" for your SPFx project and face with the following error:

No such file or directory "node_modules\@microsoft\gulp-core-build-sass\node_modules\node-sass\vendor"

Try to run the following command first:

node node_modules\@microsoft\gulp-core-build-sass\node_modules\node-sass\scripts\install.js

It will create "vendor" subfolder under "node_modules\@microsoft\gulp-core-build-sass\node_modules\node-sass" and will download necessary binaries there:

Downloading binary from https://github.com/sass/node-sass/releases/download/v4.12.0/win32-x64-64_binding.node                                                 
Download complete                                                                                                                                             
Binary saved to ...\node_modules\@microsoft\gulp-core-build-sass\node_modules\node-sass\vendor\win32-x64-64\binding.node

After that "gulp bundle" command should work.