Guidelines for Partners
When migrating a Business Central solution to the Cloud environment, issues often arise related to file management since the client’s local and server’s file systems are unavailable for the application whose target is set to Cloud. One possible way to solve the issues is to use the functionality of a standard SharePoint module, which would provide an alternative file system and replace the usage of a file management code unit. The downside is that SharePoint’s standard functionality is straightforward and limited, so this guideline provides an example of custom SharePoint file management which expands the standard SharePoint module’s functionality.
This guideline will not provide steps to set up SharePoint connector and Azure App since this information is covered in the colleague’s guideline.
Created custom objects for SharePoint File Management realization
This section describes all created custom objects and their purpose in implementing a SharePoint file management functionality.
SharePoint Setup Table and Setup Page – Table consists of fields that store information collected from the Azure application. The page intends for users to fill in values from the Azure application.
table 55039 "Sharepoint Connector Setup"
{
DataClassification = ToBeClassified;
fields
{
field(1; "Primary Key"; Code[10])
{
DataClassification = ToBeClassified;
Caption = 'Primary Key';
}
field(2; "Client ID"; Text[250])
{
DataClassification = EndUserIdentifiableInformation;
Caption = 'Client ID';
}
field(3; "Client Secret"; Text[250])
{
DataClassification = EndUserIdentifiableInformation;
Caption = 'Client Secret';
}
field(4; "Sharepoint URL"; Text[250])
{
DataClassification = ToBeClassified;
Caption = 'Sharepoint URL';
}
}
keys
{
key(PK; "Primary Key")
{
Clustered = true;
}
}
}
page 55081 "Sharepoint Connector Setup"
{
Caption = 'SharePoint Connector Setup';
PageType = Card;
ApplicationArea = All;
UsageCategory = Administration;
SourceTable = "Sharepoint Connector Setup";
ModifyAllowed = true;
InsertAllowed = false;
DeleteAllowed = false;
layout
{
area(Content)
{
group(Setup)
{
Caption = 'Setup';
field("Client ID"; Rec."Client ID")
{
ApplicationArea = All;
ToolTip = 'Specifies the Client ID. Obtained from Application''s
"SharePointConnector" Overview in Azure Portal.';
}
field("Client Secret"; Rec."Client Secret")
{
ApplicationArea = All;
ExtendedDatatype = Masked;
ToolTip = 'Specifies the Client Secret. Obtained from "Value" field after
generating client secret in Azure Portal.';
}
field("Sharepoint URL"; Rec."Sharepoint URL")
{
ApplicationArea = All;
}
}
}
}
}
SharePoint Management codeunit – Contains all SharePoint file management functions used to refactor client/server-side file system usage cases (usually related to codeunit’s 419 „File Management“ functionality).
SharePoint Request Management codeunit – Contains functions to build and send REST API requests. This codeunit is used to send only two requests: to get information about the SharePoint file to retrieve its InStream (currently, it is not possible to get the file’s InStream with standard functions) and to delete the SharePoint file (the standard module does not include a function which deletes SharePoint file).
SharePoint Operations codeunit – Contains functions that return a response to REST API request, which was sent from a function stored in the SharePoint Request Management codeunit.
SharePoint URL Builder codeunit – Contains functions used to build REST API request uri defined in the request message’s header.
SharePoint Folders List table and page – Table consists of fields that store information about the SharePoint folder. Page is used as a SharePoint file explorer. It is impossible to open the SharePoint file browser as in the local client, so the page displays all SharePoint folders with their paths to choose from.
table 55040 "Sharepoint Folder List"
{
DataClassification = ToBeClassified;
fields
{
field(1; Id; Guid)
{
Caption = 'Id';
}
field(2; Title; Text[250])
{
Caption = 'Title';
}
field(3; Created; DateTime)
{
Caption = 'Created';
}
field(5; "Server Relative Url"; Text[2048])
{
Caption = 'Server Relative Url';
}
field(6; OdataId; Text[2048])
{
Caption = 'Odata.Id';
}
}
keys
{
key(Key1; Id)
{
Clustered = true;
}
}
}
page 55082 "Sharepoint Folders List" { Caption = 'SharePoint Folders List'; PageType = List; ApplicationArea = All; SourceTable = "Sharepoint Folder List"; layout { area(Content) { repeater(List) { Caption = 'SharePoint Folders List'; field(Name; Rec.Title) { ApplicationArea = All; Width = 30; } field("Server Relative Url"; Rec."Server Relative Url") { ApplicationArea = All; Width = 150; } } } } trigger OnAfterGetCurrRecord() var Singleinstance: Codeunit "Single Instance Codeunit"; begin Singleinstance.Set_FolderPath_P55082(Rec."Server Relative Url"); end; }
Standard SharePoint module’s functions that are used in SharePoint File Management
Some SharePoint File Management functions check if provided file path begins with „/sites“ to determine if the root folder’s url is included since it is a necessary component of all SharePoint file paths. GetLists retrieves all SharePoint site’s available lists, and we constantly filter out the „Documents“ list to access the stored files. GetDocumentLibraryRootFolder function retrieves the list’s root folder. GetFolderFilesByServerRelativeUrl recovers the SharePoint site’s files by providing a folder path. GetSubFoldersByServerRelativeUrl retrieves the SharePoint site’s folders by giving a folder path.
1. Initialization
Initialization functions are stored in the SharePoint Management codeunit. These functions are already described in the colleague’s guideline (section Sharepoint connector Initialization). The only difference is that mine InitializeConnection function initializes SharePoint URL Builder to build REST API request uri, which is defined in the request message’s header, and GetSharePointAuthorization function saves created authorization code in the global variable Authorization, which is later used to authorize my two codeunit SharePoint Request Management REST API requests mentioned in the previous section. Initialization functions:
local procedure InitializeConnection()
var
SharepointSetup: Record "Sharepoint Connector Setup";
AadTenantId: Text;
Diag: Interface "HTTP Diagnostics";
SharePointList: Record "SharePoint List" temporary;
begin
if Connected then
exit;
SharepointSetup.Get();
AadTenantId := GetAadTenantNameFromBaseUrl(SharepointSetup."Sharepoint URL");
SharePointClient.Initialize(SharepointSetup."Sharepoint URL",
GetSharePointAuthorization(AadTenantId));
SharePointUriBuilder.Initialize(SharepointSetup."Sharepoint URL", 'Web');
SharePointUriBuilder.ResetPath();
SharePointClient.GetLists(SharePointList);
Diag := SharePointClient.GetDiagnostics();
if (not Diag.IsSuccessStatusCode()) then
Error(DiagError, Diag.GetErrorMessage());
Connected := true;
end;
local procedure GetSharePointAuthorization(AadTenantId: Text): Interface "SharePoint Authorization"
var
SharepointSetup: Record "Sharepoint Connector Setup";
SharePointAuth: Codeunit "SharePoint Auth.";
Scopes: List of [Text];
begin
SharepointSetup.Get();
Scopes.Add('00000003-0000-0ff1-ce00-000000000000/.default');
Authorization := SharePointAuth.CreateAuthorizationCode(AadTenantId,
SharepointSetup."Client ID", SharepointSetup."Client Secret", Scopes);
exit(Authorization);
end;
local procedure GetAadTenantNameFromBaseUrl(BaseUrl: Text): Text
var
Uri: Codeunit Uri;
MySiteHostSuffixTxt: Label '-my.sharepoint.com', Locked = true;
SharePointHostSuffixTxt: Label '.sharepoint.com', Locked = true;
OnMicrosoftTxt: Label '.onmicrosoft.com', Locked = true;
UrlInvalidErr: Label 'The Base Url %1 does not seem to be a valid SharePoint Online Url.',
Comment = '%1=BaseUrl';
Host: Text;
begin
Uri.Init(BaseUrl);
Host := Uri.GetHost();
if not Host.EndsWith(SharePointHostSuffixTxt) then
Error(UrlInvalidErr, BaseUrl);
if Host.EndsWith(MySiteHostSuffixTxt) then
exit(CopyStr(Host, 1, StrPos(Host, MySiteHostSuffixTxt) - 1) + OnMicrosoftTxt);
exit(CopyStr(Host, 1, StrPos(Host, SharePointHostSuffixTxt) - 1) + OnMicrosoftTxt);
end;
codeunit 55046 "Sharepoint URL Builder"
{
var
ServerName, Namespace : Text;
Uri: Text;
procedure Initialize(NewServerName: Text; NewNamespace: Text)
begin
ServerName := NewServerName;
Namespace := NewNamespace;
if ServerName.StartsWith('https://') then
ServerName := ServerName.Substring(9);
if ServerName.StartsWith('http://') then
ServerName := ServerName.Substring(8);
end;
procedure ResetPath()
begin
Uri := '';
end;
2. Checking if the SharePoint file exists
Function SharePointFileExists is stored in the SharePoint Management code, checking if the file exists on the SharePoint site. As the function’s parameters, it must pass a file path and a file name. In the end, SetRange filters out files by provided file name, and if the file is found function’s return parameter is set to true.
procedure SharePointFileExists(FileName: Text; FilePath: text): Boolean
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
SharepointFolder2: Record "SharePoint Folder" temporary;
SharepointFile: Record "SharePoint File";
begin
InitializeConnection();
if not FilePath.StartsWith('/sites') then
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then
if StrPos(FilePath, '/') = 1 then
FilePath := SharepointFolder."Server Relative Url" + FilePath
else
FilePath := SharepointFolder."Server Relative Url" + '/' + FilePath;
end;
SharePointClient.GetFolderFilesByServerRelativeUrl(FilePath, SharePointFile);
SharepointFile.SetRange(Name, FileName);
if SharepointFile.FindFirst() then
exit(true);
end;
The example below shows how to refactor codeunit’s 419 “File Management” standard function ServerFileExists by using the SharePointFileExists function:
Var
_NewServerFile: Text;
_ErrFileAlreadyExist: Label 'A file with the same name already exists, please rename the file
before attaching';
SharePointMgt: Codeunit "Sharepoint Management";
_FileMgt: Codeunit "File Management";
//if _FileMgt.ServerFileExists(_NewServerFile) then
FileUploaded := UploadIntoStream(_TextPickupFile, '', Filter, _TempServerFileName, FileInStream);
if SharePointMgt.SharePointFileExists(_TempServerFileName, _NewServerFile) then
Error(_ErrFileAlreadyExist);
3. Saving a file
Function SaveFile is stored in the SharePoint Management codeunit, and it’s used to save a file in the SharePoint site. As the function’s parameters, it is required to pass a file name, path, and an InStream. If the file is saved, the function returns the file path of the newly created file.
procedure SaveFile(FileName: Text; FilePath: text; FileInStream: InStream) NewFilePath: Text;
var
SharePointFile: Record "SharePoint File" temporary;
Diag: Interface "HTTP Diagnostics";
FileDirectory: Text;
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
begin
InitializeConnection();
if not FilePath.StartsWith('/sites') then
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then
if StrPos(FilePath, '/') = 1 then
FilePath := SharepointFolder."Server Relative Url" + FilePath
else
FilePath := SharepointFolder."Server Relative Url" + '/' + FilePath;
end;
if SharePointClient.AddFileToFolder(FilePath, FileName, FileInStream, SharePointFile) then
begin
NewFilePath := SharePointFile."Server Relative Url";
end else begin
Diag := SharePointClient.GetDiagnostics();
if (not Diag.IsSuccessStatusCode()) then
Error(DiagError, Diag.GetErrorMessage());
end;
end
4. Creating a directory
Function CreateSharePointDirectory is stored in the SharePoint Management codeunit and used to create a new directory in the SharePoint site. Per the function’s parameters, passing a folder path and an empty SharePoint Folder variable is required. After the function’s completion, the passed SharePoint Folder variable will return the newly created folder, which contains the path of the newly created directory. The Standard CreateFolder function creates a new sub-folder in the SharePoint site by providing a folder path. New created folder is returned as the second function’s CreateFolder parameter.
procedure CreateSharePointDirectory(FolderPath: Text; var NewSharepointFolder: Record "SharePoint
Folder" temporary)
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
SharepointFolder2: Record "SharePoint Folder" temporary;
SharepointFolderEmpty: Record "SharePoint Folder" temporary;
RelativeUrl: Text;
FolderName: Text;
Filter: Text;
SharePointFile: Record "SharePoint File" temporary;
FInStream: InStream;
begin
InitializeConnection();
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then
RelativeUrl := SharepointFolder."Server Relative Url";
end;
if FolderPath.StartsWith('/sites') then
FolderPath := DelStr(FolderPath, 1, StrLen(RelativeUrl));
if FolderPath <> '' then
if StrPos(FolderPath, '/') = 1 then
FolderPath := DelStr(FolderPath, 1, 1);
repeat
if FolderPath <> '' then begin
if StrPos(FolderPath, '/') = 0 then begin
FolderName := FolderPath;
if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
SharepointFolder2) then begin
if SharepointFolder2.Exists then begin
Filter := '@' + RelativeUrl + '/' + FolderName;
SharepointFolder2.SetFilter("Server Relative Url", Filter);
if not SharepointFolder2.FindFirst() then
SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
NewSharepointFolder)
else
NewSharepointFolder := SharepointFolder2;
end else
SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
NewSharepointFolder);
end;
RelativeUrl := NewSharepointFolder."Server Relative Url";
FolderPath := '';
end else begin
FolderName := CopyStr(FolderPath, 1, StrPos(FolderPath, '/') - 1);
if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
SharepointFolder2) then begin
if SharepointFolder2.Exists then begin
Filter := '@' + RelativeUrl + '/' + FolderName;
SharepointFolder2.SetFilter("Server Relative Url", Filter);
if not SharepointFolder2.FindFirst() then
SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
NewSharepointFolder)
else
NewSharepointFolder := SharepointFolder2;
end else
SharePointClient.CreateFolder(RelativeUrl + '/' + FolderName,
NewSharepointFolder);
end;
RelativeUrl := NewSharepointFolder."Server Relative Url";
FolderPath := CopyStr(FolderPath, StrPos(FolderPath, '/') + 1, StrLen(FolderPath)-
StrLen(CopyStr(FolderPath, 1, StrPos(FolderPath, '/'))));
end;
end;
SharepointFolder2 := SharepointFolderEmpty;
until FolderPath = '';
end;
The example below shows how to refactor codeunit’s 419 “File Management” standard functions ServerDirectoryExists and ServerCreateDirectory using the function CreateSharePointDirectory. CreateSharePointDirectory creates a directory only if the same directory does not exist, so using a separate function to check the folder path is unnecessary.
var
SharePointMgt: Codeunit "Sharepoint Management";
NewSharepointFolder: Record "SharePoint Folder" temporary;
AttachmentFolder: Text;
// _NewServerDirectory := StrSubstNo('%1%2\%3', AttachmentFolder,
// _CompanyCodes.GetCurrentCompanyIntercoCode, Date2DMY(Today, 3));
// if not _FileMgt.ServerDirectoryExists(_NewServerDirectory) then
// _FileMgt.ServerCreateDirectory(_NewServerDirectory);
SharePointMgt.CreateSharePointDirectory(AttachmentFolder + '/' +
_CompanyCodes.GetCurrentCompanyIntercoCode + '/' + Format(Date2DMY(Today, 3)), NewSharepointFolder);
_NewServerDirectory := NewSharepointFolder."Server Relative Url";
5. Checking if the SharePoint directory exists
Function SharePointDirectoryExists is stored in the SharePoint Management codeunit until it checks if a directory exists in the SharePoint site. Per the function’s parameters, passing a folder path and an empty text variable is required. After the function’s completion, if the
directory is found, the passed empty text variable will return the path of the found directory, and the function’s return parameter will be set to true. The second parameter of this function is helpful in cases where a provided directory for checking does not contain the SharePoint root folder’s path – for example, after running the function SharePointDirectoryExists, returned second parameter’s value could be used for saving files.
procedure SharePointDirectoryExists(DirectoryPath: Text; var SharepointDirectory: Text): Boolean
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
RelativeUrl: Text;
Filter: Text;
FolderName: Text;
NewSharepointFolder: Record "SharePoint Folder" temporary;
begin
InitializeConnection();
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then
RelativeUrl := SharepointFolder."Server Relative Url";
end;
if DirectoryPath.StartsWith('/sites') then
DirectoryPath := DelStr(DirectoryPath, 1, StrLen(RelativeUrl));
if DirectoryPath <> '' then
if StrPos(DirectoryPath, '/') = 1 then
DirectoryPath := DelStr(DirectoryPath, 1, 1);
repeat
if DirectoryPath <> '' then begin
if StrPos(DirectoryPath, '/') = 0 then begin
FolderName := DirectoryPath;
if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
NewSharepointFolder) then begin
if NewSharepointFolder.Exists then begin
Filter := '@' + RelativeUrl + '/' + FolderName;
NewSharepointFolder.SetFilter("Server Relative Url", Filter);
if not NewSharepointFolder.FindFirst() then
exit(false);
end;
end;
RelativeUrl := NewSharepointFolder."Server Relative Url";
DirectoryPath := '';
end else begin
FolderName := CopyStr(DirectoryPath, 1, StrPos(DirectoryPath, '/') - 1);
if SharePointClient.GetSubFoldersByServerRelativeUrl(RelativeUrl,
NewSharepointFolder) then begin
if NewSharepointFolder.Exists then begin
Filter := '@' + RelativeUrl + '/' + FolderName;
NewSharepointFolder.SetFilter("Server Relative Url", Filter);
if not NewSharepointFolder.FindFirst() then
exit(false);
end;
end;
RelativeUrl := NewSharepointFolder."Server Relative Url";
DirectoryPath := CopyStr(DirectoryPath, StrPos(DirectoryPath, '/') + 1,
StrLen(DirectoryPath) - StrLen(CopyStr(DirectoryPath, 1, StrPos(DirectoryPath,
'/'))));
end;
end;
until DirectoryPath = '';
SharepointDirectory := RelativeUrl;
exit(true);
end;
The example below shows how to refactor codeunit’s 419 “File Management” standard function ServerDirectoryExists by using the function SharePointDirectoryExists:
//if not FileMgt.ServerDirectoryExists(ImportFolder) then
// Error(_ErrFolderNotExist, ImportFolder ");
if not SharePointMgt.SharePointDirectoryExists(ImportFolder, SharepointDirectory) then
Error(_ErrFolderNotExist, ImportFolder);
6. Getting files from a specified folder path
Function GetFilesFromPath is stored in SharePoint Management codeunit and retrieves files from a specified directory. As per the function’s parameters, passing a folder path and an empty SharePoint File variable is required. After the function’s completion, the passed empty SharePoint variable will return the files list from a specified directory.
procedure GetFilesFromPath(DirectoryPath: Text; var SharePointFile: Record "SharePoint File"
temporary)
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
RelativeUrl: Text;
begin
InitializeConnection();
if not DirectoryPath.StartsWith('/sites') then begin
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then begin
if StrPos(DirectoryPath, '/') = 1 then
RelativeUrl := SharepointFolder."Server Relative Url" + DirectoryPath
else
RelativeUrl := SharepointFolder."Server Relative Url" + '/' +
DirectoryPath;
end;
end;
end else
RelativeUrl := DirectoryPath;
SharePointClient.GetFolderFilesByServerRelativeUrl(RelativeUrl, SharePointFile);
end;
The example below shows how to refactor DotNet files retrieving functions functionality by using the function GetFilesFromPath:
var
//SPLN1.00 - Start
// [RunOnClient]
// ClientDirectoryInfo: DotNet DirectoryInfo;
// [RunOnClient]
// ClientEnumerator: DotNet IEnumerator;
// [RunOnClient]
// ClientFileInfo: DotNet FileInfo;
// [RunOnClient]
// ClientList: DotNet List_Of_T;
ImportFolder: Text;
SharePointMgt: Codeunit "Sharepoint Management";
SharePointFile: Record "SharePoint File" temporary;
begin
_Window.Open(Text001);
//read the local folder to retrieve pdf files list
//SPLN1.00 - Start
// ClientDirectoryInfo := ClientDirectoryInfo.DirectoryInfo(ImportFolder);
// ClientList := ClientDirectoryInfo.GetFiles('*.pdf');
// ClientEnumerator := ClientList.GetEnumerator();
SharePointMgt.GetFilesFromPath(ImportFolder, SharePointFile)
SharePointFile.SetFilter(Name, '@*.pdf');
// while ClientEnumerator.MoveNext do begin
if SharePointFile.FindSet() then
repeat
The second example shows a workaround with the function GetFilesFromPath when the OnPrem table „File“ is being filtered by a folder path:
var
//SPLN1.00 - Start
//_FileList: Record File;
ImportFolder: Text;
SharePointMgt: Codeunit "Sharepoint Management";
SharePointFile: Record "SharePoint File" temporary;
//_FileList.SetRange(Path, ImportFolder);
//_FileList.SetRange("Is a file", true);
//_FileList.SetFilter(Name, '*.csv*');
//_FileList.SetFilter(Size, '>0');
SharePointMgt.GetFilesFromPath(ImportFolder, SharePointFile);
SharePointFile.SetRange(Exists, true);
SharePointFile.SetFilter(Name, '*.csv*');
SharePointFile.SetFilter(Length, '>0');
//if _FileList.FindSet then
if SharePointFile.FindSet then
7. Getting a file
Function GetFile is stored in SharePoint Management codeunit and returns a specified file. As the function’s parameters, it is required to pass a directory path, a file name, and an empty SharePoint File variable that will return the retrieved file after the function’s completion. GetFile function is mostly used in combination with other SharePoint Management functions (This will be shown in the examples later).
procedure GetFile(DirectoryPath: Text; FileName: Text; var ResultSharePointFile: Record
"SharePoint File" temporary)
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
SharePointFile: Record "SharePoint File" temporary;
RelativeUrl: Text;
begin
InitializeConnection();
if not DirectoryPath.StartsWith('/sites') then begin
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
if SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId,
SharePointFolder) then begin
if StrPos(DirectoryPath, '/') = 1 then
RelativeUrl := SharepointFolder."Server Relative Url" + DirectoryPath
else
RelativeUrl := SharepointFolder."Server Relative Url" + '/' +
DirectoryPath;
end;
end;
end else
RelativeUrl := DirectoryPath;
SharePointClient.GetFolderFilesByServerRelativeUrl(RelativeUrl, SharePointFile);
SharePointFile.SetRange(Name, FileName);
if SharePointFile.FindFirst() then
ResultSharePointFile := SharePointFile;
end;
8. Getting an InStream of a file
Function GetFileContentInStream is stored in SharePoint Management codeunit and returns an InStream of a specified file. Function GetFileContentInStream is stored in SharePoint Management codeunit and returns an InStream of a specified file.
procedure GetFileContentInStream(OdataId: Text; var FileInStream: InStream): Boolean
begin
SharePointUriBuilder.ResetPath(OdataId);
SharePointUriBuilder.SetObject('$value');
SharePointRequestHelper.SetAuthorization(Authorization);
SharePointOperationResponse := SharePointRequestHelper.Get(SharePointUriBuilder);
if not SharePointOperationResponse.GetDiagnostics() then
exit(false);
SharePointOperationResponse.GetResultAsStream(FileInStream);
exit(true);
end;
GetFileContentInStream uses:
Procedures ResetPath, SetObject, and GetUri of SharePoint URL Builder codeunit to build REST API request uri, which has to be defined in the request message’s header:
var
Uri: Text;
UriAppendTxt: Label '/%1', Comment = '%1 - URI part to append', Locked = true;
procedure SetObject(Object: Text)
begin
Uri += StrSubstNo(UriAppendTxt, EscapeDataString(Object));
end;
local procedure EscapeDataString(TextToEscape: Text): Text
var
LocalUri: Codeunit Uri;
begin
exit(LocalUri.EscapeDataString(TextToEscape));
end;
var
ServerName, Namespace : Text;
Uri: Text;
UriLbl: Label 'https://{server_name}/_api/{namespace}', Locked = true;
procedure GetUri(): Text
var
FullUri: Text;
begin
FullUri := UriLbl + Uri + '/';
FullUri := FullUri.Replace('{server_name}',ServerName.TrimStart('/').TrimEnd('/')).Replace('{namespace}',
Namespace.TrimStart('/').TrimEnd('/'));
exit(FullUri);
end;
procedure ResetPath(Id: Text)
begin
Uri := UriLbl;
Uri := Uri.Replace('{server_name}',
ServerName.TrimStart('/').TrimEnd('/')).Replace('{namespace}',Namespace.TrimStart('/').TrimEnd('/'));
Uri := Id.Replace(Uri, '');
end;
Procedures SetAuthorization, Get, PrepareRequestMsg, SendRequest of SharePoint Request Management codeunit to authorize, build and send REST API request:
var
HttpClient: HttpClient;
Authorization: Interface "SharePoint Authorization";
OperationNotSuccessfulErr: Label 'An error has occurred';
UserAgentLbl: Label 'NONISV|%1|Dynamics 365 Business Central - %2/%3', Locked = true, Comment = '%1 = App
Publisher; %2 = App Name; %3 = App Version';
procedure SetAuthorization(Auth: Interface "SharePoint Authorization")
begin
Authorization := Auth;
end;
procedure Get(SharePointUriBuilder: Codeunit "SharePoint URL Builder") OperationResponse: Codeunit "Sharepoint Operations"
begin
OperationResponse := SendRequest(PrepareRequestMsg("Http Request Type"::GET, SharePointUriBuilder));
end;
local procedure PrepareRequestMsg(HttpRequestType: Enum "Http Request Type"; SharePointUriBuilder: Codeunit "SharePoint URL Builder") RequestMessage: HttpRequestMessage
var
Headers: HttpHeaders;
Begin
RequestMessage.Method(Format(HttpRequestType));
RequestMessage.SetRequestUri(SharePointUriBuilder.GetUri());
RequestMessage.GetHeaders(Headers);
Headers.Add('Accept', 'application/json');
Headers.Add('User-Agent', GetUserAgentString());
end;
local procedure SendRequest(HttpRequestMessage: HttpRequestMessage) OperationResponse: Codeunit "Sharepoint Operations"
var
HttpResponseMessage: HttpResponseMessage;
Content: Text;
Begin
Authorization.Authorize(HttpRequestMessage);
if not HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then
Error(OperationNotSuccessfulErr);
HttpResponseMessage.Content.ReadAs(Content);
OperationResponse.SetHttpResponse(HttpResponseMessage);
end;
Procedures GetResultAsStream, GetDiagnostics of SharePoint Operations codeunit to get diagnostics, resulting in InStream of sent REST API requests. The remaining functions read InStream and header values from the response message.
procedure GetResultAsStream(var ResultInStream: InStream)
begin
TempBlobContent.CreateInStream(ResultInStream);
end;
procedure SetHttpResponse(HttpResponseMessage: HttpResponseMessage)
var
ContentOutStream: OutStream;
ContentInStream: InStream;
begin
TempBlobContent.CreateOutStream(ContentOutStream);
HttpResponseMessage.Content().ReadAs(ContentInStream);
CopyStream(ContentOutStream, ContentInStream);
HttpHeaders := HttpResponseMessage.Headers();
SetParameters(HttpResponseMessage.IsSuccessStatusCode, HttpResponseMessage.HttpStatusCode,
HttpResponseMessage.ReasonPhrase, GetRetryAfterHeaderValue(), GetErrorDescription());
end;
procedure GetHeaderValueFromResponseHeaders(HeaderName: Text): Text
var
Values: array[100] of Text;
begin
if not HttpHeaders.GetValues(HeaderName, Values) then
exit('');
exit(Values[1]);
end;
procedure GetRetryAfterHeaderValue() RetryAfter: Integer;
var
HeaderValue: Text;
begin
HeaderValue := GetHeaderValueFromResponseHeaders('Retry-After');
if HeaderValue = '' then
exit(0);
if not Evaluate(RetryAfter, HeaderValue) then
exit(0);
end;
local procedure GetErrorDescription(): Text
var
Result: Text;
JObject: JsonObject;
JToken: JsonToken;
begin
GetResultAsText(Result);
if Result <> '' then
if JObject.ReadFrom(Result) then
if JObject.Get('error_description', JToken) then
exit(JToken.AsValue().AsText());
end;
procedure GetDiagnostics(): Boolean
begin
exit(IsSuccessStatusCode);
end;
procedure SetParameters(NewIsSuccesss: Boolean; NewHttpStatusCode: Integer; NewResponseReasonPhrase: Text;
NewRetryAfter: Integer; NewErrorMessage: Text)
begin
SuccessStatusCode := NewIsSuccesss;
HttpStatusCode := NewHttpStatusCode;
ResponseReasonPhrase := NewResponseReasonPhrase;
RetryAfter := NewRetryAfter;
ErrorMessage := NewErrorMessage;
end;
procedure IsSuccessStatusCode(): Boolean
begin
exit(SuccessStatusCode);
end;
var
TempBlobContent: Codeunit "Temp Blob";
HttpHeaders: HttpHeaders;
ErrorMessage: Text;
ResponseReasonPhrase: Text;
HttpStatusCode: Integer;
RetryAfter: Integer;
SuccessStatusCode: Boolean;
GetFileContentInStream function is mostly used in combination with other SharePoint Management functions, for example, to copy a file from one directory to another (Usage of this function will be shown in the examples later). As the function’s parameters, it is required to pass an Odata id, a parameter of the filing entity, and an InStream that will be populated with the file content. I created this function since the standard BC SharePoint module did not have a procedure to get the InStream of a file until the latest „Business Central“release, 22.2. In BC 22.2 function to get the file’s InStream is called DownloadFileContent.
procedure DownloadFileContent(OdataId: Text; var FileInStream: InStream): Boolean
begin
exit(SharePointClientImpl.DownloadFileContent(OdataId, FileInStream));
end;
9. Delete a file
Function DeleteFile is stored in the SharePoint Management codeunit and deletes a specified file from the SharePoint site. As the function’s parameters, it is required to pass Odata id, which is a parameter of the file entity. The Standard SharePoint module does not have a function that deletes a file from the SharePoint site; therefore, as with the GetFileContentInStream procedure, it was necessary to build and send a custom REST API request.
procedure DeleteFile(OdataId: Text): Boolean
begin
SharePointUriBuilder.ResetPath(OdataId);
SharePointRequestHelper.SetAuthorization(Authorization);
SharePointOperationResponse := SharePointRequestHelper.Delete(SharePointUriBuilder);
if not SharePointOperationResponse.GetDiagnostics() then
exit(false);
exit(true);
end;
DeleteFile uses:
- Procedure ResetPath of SharePoint URL Builder codeunit to build REST API request uri, which must be defined in the request message’s header (image displayed in a section of GetFileContentInStream function).
- Procedures SetAuthorization, Delete, PrepareRequestMsg, SendRequestDelete of SharePoint Request Management codeunit to authorize, build and send REST API request (image of procedure SetAuthorization is displayed in a section of GetFileContentInStream function):
procedure Delete(SharePointUriBuilder: Codeunit "SharePoint URL Builder") OperationResponse: Codeunit
"Sharepoint Operations"
begin
OperationResponse := SendRequestDelete(PrepareRequestMsg("Http Request Type"::DELETE,
SharePointUriBuilder));
end;
local procedure SendRequestDelete(HttpRequestMessage: HttpRequestMessage) OperationResponse: Codeunit
"Sharepoint Operations"
var
HttpResponseMessage: HttpResponseMessage;
Content: Text;
begin
Authorization.Authorize(HttpRequestMessage);
if not HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then
Error(OperationNotSuccessfulErr);
OperationResponse.SetHttpResponse(HttpResponseMessage);
end;
local procedure PrepareRequestMsg(HttpRequestType: Enum "Http Request Type"; SharePointUriBuilder: Codeunit
"SharePoint URL Builder") RequestMessage: HttpRequestMessage
Var
Headers: HttpHeaders;
Begin
RequestMessage.Method(Format(HttpRequestType));
RequestMessage.SetRequestUri(SharePointUriBuilder.GetUri());
RequestMessage.GetHeaders(Headers);
Headers.Add('Accept', 'application/json');
Headers.Add('User-Agent', GetUserAgentString());
end;
- Procedures GetDiagnostics and SetHttpResponse of SharePoint Operations codeunit to get diagnostics and read response message header values (images of these procedures are displayed in a section of the GetFileContentInStream function).
10. Selecting a SharePoint folder (SharePoint file explorer)
Function SelectSharePointFolder is stored in the SharePoint Management codeunit, allowing the user to select a SharePoint site folder from the displayed folders list on a page (SharePoint Folders List page). As the function’s parameter, passing the Text variable that will be populated with the selected folder’s path value is required. SelectSharePointFolder calls the function BuildFoldersList, which finds every SharePoint site’s folder and returns a compiled list of folders.
procedure SelectSharePointFolder(var SelectedFolder: Text): Boolean
var
SharePointList: Record "SharePoint List" temporary;
SharepointFolder: Record "SharePoint Folder" temporary;
SharePointListItem: Record "SharePoint List Item" temporary;
NewSharepointFolder: Record "SharePoint Folder" temporary;
SharePointFolderListPage: Page "Sharepoint Folders List";
Singleinstance: Codeunit "Single Instance Codeunit";
begin
InitializeConnection();
if SharePointClient.GetLists(SharePointList) then begin
SharePointList.SetRange(Title, 'Documents');
if SharePointList.FindFirst() then
SharePointClient.GetDocumentLibraryRootFolder(SharePointList.OdataId, SharePointFolder);
end;
SharePointFolderList.DeleteAll();
BuildFoldersList(SharePointFolder);
if Page.RunModal(Page::"Sharepoint Folders List", SharePointFolderList) = Action::LookupOK then begin
SelectedFolder := Singleinstance.Get_FolderPath_P55082();
exit(true);
end;
end;
procedure BuildFoldersList(var NewSharepointFolder: Record "SharePoint Folder" temporary)
var
SharepointFolder2: Record "SharePoint Folder" temporary;
begin
SharePointFolderList.Init();
SharePointFolderList.Id := NewSharepointFolder."Unique Id";
SharePointFolderList.Title := NewSharepointFolder.Name;
SharePointFolderList.OdataId := NewSharepointFolder.OdataId;
SharePointFolderList."Server Relative Url" := NewSharepointFolder."Server Relative Url";
SharePointFolderList.Created := NewSharepointFolder.Created;
SharePointFolderList.Insert();
SharePointClient.GetSubFoldersByServerRelativeUrl(NewSharepointFolder."Server Relative Url",
SharepointFolder2);
if SharepointFolder2.Exists then begin
if SharepointFolder2.FindSet() then
repeat
NewSharepointFolder := SharepointFolder2;
BuildFoldersList(NewSharepointFolder);
until SharepointFolder2.Next() = 0;
end else
exit;
end;
var
SharePointFolderList: record "Sharepoint Folder List" temporary;
Examples below show how to refactor codeunit’s 419 “File Management” standard functions BrowseForFolderDialog and SelectFolderDialog by using the function SelectSharePointFolder:
//SPLN1.00 - Start
//PickedFolder := FileMgt.BrowseForFolderDialog(TextChooseFolder, PickedFolder, true);
SharePointMgt.SelectSharePointFolder(PickedFolder);
//SPLN1.00 - End
//SPLN1.00 - Start
//if _FileMgt.SelectFolderDialog(TextSelectFolder, _TempFolderName) then
if SharePointMgt.SelectSharePointFolder(_TempFolderName) then
//SPLN1.00 - End
11. Examples of SharePoint File Management usage
11.1. Copying file to a different directory
Since it is not possible to copy the file to a different directory with standard SharePoint functionality, firstly, we get the InStream of a file we want to copy with the procedure GetFileContentInStream and then use retrieved InStream to create a new file in the desired directory.
_PreviousFileName := _FileMgt.GetFileNameWithoutExtension(_FileName);
_PreviousFileExtension := _FileMgt.GetExtension(_FileName);
_NewFileName := _ArchiveFolder + '/' + _PreviousFileName + '_' + Format(CurrentDateTime, 0,
'<Year4><Month,2><Day,2><Hours24><Minutes,2>') + '.' + _PreviousFileExtension;
SharePointMgt.GetFileContentInStream(OdataId, FileInStream);
SharePointMgt.SaveFile(_PreviousFileName + '_' + Format(CurrentDateTime, 0,
'<Year4><Month,2><Day,2><Hours24><Minutes,2>') + '.' + _PreviousFileExtension, _ArchiveFolder, FileInStream);
//Copy(_FileName, _NewFileName);
11.2. Copying file to a different directory and deleting the old one
First, with the function GetFile, we acquire the file we want to copy to get its InStream. Then with the function, GetFileContentInStream, retrieved InStream is used to create a new file in the desired directory. After checking if the new file was created with the function SharePointFileExists, the file in the old directory is deleted by the function DeleteFile.
// FileSystemObject.CopyFile(_FromFile, _ToFile, false);
// if FileSystemObject.FileExists(_ToFile) then
// FileSystemObject.DeleteFile(_FromFile);
SharePointMgt.GetFile(SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_FromFile)),
FileMgmt.GetFileName(_FromFile), OldSharePointFile);
SharePointMgt.GetFileContentInStream(OldSharePointFile.OdataId, FileInStream);
SharePointMgt.SaveFile(FileMgmt.GetFileName(_ToFile),
SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_ToFile)), FileInStream);
if SharePointMgt.SharePointFileExists(FileMgmt.GetFileName(_ToFile),
SharePointMgt.FixPathForSharePoint(FileMgmt.GetDirectoryName(_ToFile))) then
SharePointMgt.DeleteFile(OldSharePointFile.OdataId);
//SPLN1.00 - End
Function FixPathForSharePoint fixes the path for SharePoint use since standard file management functions by default turn slashes “/” to a different side than used in SharePoint (local client’s path’s slash “\”).
procedure FixPathForSharePoint(FolderPath: Text): Text
var
SPFolderPath: Text;
begin
SPFolderPath := FolderPath.Replace('\', '/');
SPFolderPath := SPFolderPath.Replace('//', '/');
if StrPos(SPFolderPath, ':') > 0 then
SPFolderPath := DelStr(SPFolderPath, 1, StrPos(SPFolderPath, ':'));
if SPFolderPath.EndsWith('/') then
SPFolderPath := DelStr(SPFolderPath, StrLen(SPFolderPath), 1);
exit(SPFolderPath);
end;
Conclusion
One solution for solving issues related to the local file system’s usage is using a standard SharePoint module. SharePoint module provides an alternative file system but has a significant drawback – the standard module’s functionality is simple and very limited (e.g., common file copying or removing functions are missing). This guideline gives an example of SharePoint file management implementation, which replicates various “OnPrem” file management functionalities to expand the standard SharePoint module’s features. Looking on the bright side, the newly added function (file’s InStream download) to the standard SharePoint module in the latest „Business Central“ release shows that standard SharePoint functionality might be broadened in the future.