Build a web app to manage a custom provider in Viva Learning with Blazor
Published Nov 29 2022 07:57 AM 1,743 Views
Microsoft

In the previous post we have learned the basic concepts behind the Viva Learning integration offered by the Microsoft Graph. However, the scenario we implemented wasn't very realistic. We have learned which APIs to use and how to use them but, in a real scenario, you won't use Postman to manage your custom catalog of learning content, but you would rely on a more robust solution.

 

In this post, we're going to reuse the concepts we have learned to build a better experience: a web application, that we can use to manage our custom learning provider and its contents. We'll focus on how to implement in a real application some of the peculiar features we have learned about these APIs, like the fact that a different set of permissions is required based on the type of content you're working with.

 

Since I'm a .NET developer, I'm going to build the web application using Blazor, the latest addition to the .NET family which enables us to build client-side web apps using C# instead of JavaScript as a programming language. In this post, I'm going to assume you already have a basic knowledge of Blazor and basic ASP.NET Core concepts, like Razor components and dependency injection.

 

Let's start from the basics: adding authentication and authorization.

 

Setting up the Blazor app

Blazor comes into two different flavors: fully client side, using Web Assembly, and server side. The final experience is exactly the same and both approaches enables us to build client side applications using C#. In the first case, however, the application is truly client-side only, since it runs directly in the browser. In the second case, instead, the application is still client-side, but it's backed by a server side application, which takes care of "translating" the C# code into JavaScript through a Signal R channel. For our project, we're going to use the server side approach, since it enables us to fully use the Microsoft Identity platform capabilities and features.

 

Open Visual Studio, create a new project and look for the template called Blazor Server App. When you reach the Additional information step, feel free to pick .NET 6 or .NET 7 based on your requirements (.NET 7 is newer, but .NET 6 is marked as LTS, so it will be supported until 2024). What's important is that, in the Authentication Type dropdown, you pick Microsoft Identity Platform.

 

new-blazor-project.png

 

After you hit the Create button, Visual Studio will scaffold the project. However, before starting to work on the code, you'll be asked first to set up the connection to the Microsoft Identity platform. As the first step, the wizard will install the required msidentity tool, which is a .NET extension that simplifies the integration of Azure Active Directory into your application. Once the tools are installed, you will be asked to pick the account where your app registration on Azure Active Directory lives (or where you want to create a new one). If you have followed the previous post, we have already had one app registration: it's the one we have used with Postman. It's essential that you use it also for the Blazor app, otherwise things won't work properly. If you remember, one of the requirements to work with the Viva Learnings APIs is that all the operations must be performed using the same application.

 

app-registration.png

 

Once you hit Next, you'll be able to configure Microsoft Identity not just to authenticate the user, but also to connect to additional APIs. In our case, we need to support the Microsoft Graph, so enable the Add Microsoft Graph check:

 

microsoft-graph.png

 

The next and last step is to import the client secret which is required to connect to the Azure AD app. The tool will create a key called ClientSecret in the AzureAd section of the configuration file and it will store the secret automatically retrieved from the portal. Additionally, it won't store the information in the plain configuration file, but in the local user secrets file, called secrets.jsonThis feature helps you to protect your sensitive information on your development machine, since the secrets.json file, even if it's read at runtime and merged into the standard configuration like a regular appsettings.json file, lives outside the project's folder. This means that you don't risk storing the client secret in your repository when you commit the solution.

client-secret.png

Thanks to the Visual Studio integration with the Microsoft Identity platform, at the end of the wizard you will have all the required building blocks in place:

 

  • Inside the appsettings.json file, you will find a section called AzureAd with all the information needed to connect to your Azure AD app, like client id, tenant id, etc.
  • Inside the Program.cs file, you will see the code required to register the different services needed to authenticate your application and authorize the different operations.

Here is the full startup code:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using VivaLearningApp.Data;
using VivaLearningApp.Services;

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);

var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
    .AddInMemoryTokenCaches();

builder.Services.AddControllersWithViews().
    AddMicrosoftIdentityUI();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

This integration is made easier by the Microsoft.Identity.Web library which, as the name says, simplifies the integration of the Microsoft Identity platform in a web application. In the first lines, in fact, you can see how the integration is simply enabled by the AddMicrosoftIdentityWebApp() method which receives, as input, the section of our configuration called AzureAd, which contains all the information about our Azure AD app. A few lines below you can notice also how the authorization is configured to use the default policy whenever the user isn't authenticated. This means that your application can't be used in anonymous mode. As soon as they launch the web application, they will be asked to login to move forward.

 

You can easily test this by simply launching the debugger by pressing F5. The Blazor web application will be launched locally and, as a first thing, you will be asked to login with a work account from your Microsoft 365 tenant. Only if you login, you'll land on the home page and you'll be able to see your account:

 

user-logged-in.png

 

Before we start building our page to work with the custom content provider, however, we must make a change to the project's configuration. Since, during the wizard, we have enabled the integration with the Microsoft Graph, Visual Studio automatically added for us the package Microsoft.Identity.Web.MicrosoftGraph, which simplifies getting an authenticated Microsoft Graph client. However, in the previous post, we have learned that the Viva Learning APIs are part of the beta endpoint, as such we must make a couple of changes to our .csproj file:

 

  • Replace the Microsoft.Identity.Web.MicrosoftGraph library with the equivalent beta version, which is Microsoft.Identity.Web.MicrosoftGraphBeta.

    <PackageReference Include="Microsoft.Identity.Web.MicrosoftGraphBeta" Version="1.24.1" />
    
  • Add the Microsoft.Graph.Beta package to the project, which is the beta version of the Microsoft Graph .NET SDK:

    <PackageReference Include="Microsoft.Graph.Beta" Version="4.66.0-preview" />
    

    In a regular scenario, this step wouldn't be required since the Microsoft Graph .NET SDK already comes with the Microsoft.Identity.Web.MicrosoftGraph package as a dependency. However, in our scenario, it's necessary because this built-in version isn't up-to-date, and it doesn't include the Viva Learning endpoints yet. Please also note the version number, which is 4.66.0-preview. Make sure to use this version and not one of the latest 5.x versions, because they contain a lot of breaking changes that aren't supported by the Microsoft.Identity.Web library at the time of writing.

Now we have all the basic building blocks to start the integration of the Microsoft Graph APIs.

 

Listing the custom learning providers

As the first step, we're going to build a Razor component that displays the list of available learning providers. However, the list will contain only one item: as we have learned in the previous post, currently the Viva Learning APIs support having only a single custom provider.

Before starting to build the page, however, let's create a class we're going to use to centralize all our operations with the Microsoft Graph. Right click on your project, choose Add → Class and give it a meaningful name, like CustomGraphService. As the first step, we're going to add the method we need to get the list of custom providers. Thanks to the Microsoft Graph integration provided by the Microsoft Identity Web library, we don't need to create our own instance of the Graph client, but it's automatically injected inside the DI container of our Blazor web app. All we need to do is add two dependencies to the public constructor of our class, as in the following example:

public class CustomGraphService
{
    private readonly GraphServiceClient delegatedClient;
    private readonly MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler;

    public CustomGraphService(GraphServiceClient delegatedClient, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
    {
        this.delegatedClient = delegatedClient;
        this.consentHandler = consentHandler;
    }
}

GraphServiceClient is the Graph client provided the Microsoft Graph .NET SDK. We can use it to perform any operation against the Microsoft Graph in a object oriented way. Instead of manually building the HTTP requests and manually parsing JSON responses, we can directly use classes and objects which represent the various endpoints and entities in the Microsoft Graph. Thanks to the Microsoft Identity Web library, the GraphServiceClient object we get is already authenticated: it's already using the proper access token retrieved when we logged in with our work account. MicrosoftIdentityConsentAndConditionalAccessHandler, instead, is a helper class provided by the Microsoft Identity Web library that we can use to wrap our Microsoft Graph operations. It makes sure that the proper consent is requested in case of issues with the access token.

Now that we have these blocks, we can create a method which returns the list of custom providers:

public async Task<IList<LearningProvider>> GetLearningProvidersAsync()
{
    try
    {
        var result = await delegatedClient.EmployeeExperience.LearningProviders.Request().GetAsync();
        return result.CurrentPage;
    }
    catch (Exception ex)
    {
        consentHandler.HandleException(ex);
        return null;
    }
}

As you can see, the Microsoft Graph .NET SDK makes it easier to consume the Graph from a .NET application. The https://graph.microsoft.com/beta/employeeExperience/learningProviders endpoint is mapped with the EmployeeExperience.LearningProviders.Request() object. By calling the GetAsync() method, we are performing a HTTP GET request. The response is a collection of LearningProvider objects, which have the same properties we have seen in the JSON response in the previous post, like id, name and logos. In this method you can also see the MicrosoftIdentityConsentAndConditionalAccessHandler helper in action. We wrap the Graph operation in a try / catch statement and, in case it fails, we use the helper to try again the request with the proper permissions.

Now we're ready to use our class. First, however, we need to register it into the Blazor dependency injection container, so that in our Razor component we can retrieve an instance with all the dependencies already satisfied. First, let's create an interface that describes our class:

public interface ICustomGraphService
{
    Task<IList<LearningProvider>> GetLearningProvidersAsync();
}

Now, let's change our class to inherit from this interface:

public class CustomGraphService : ICustomGraphService
{
    // class implementation
}

Please note! During the post we're going to add additional methods to the CustomGraphService class. Remember to declare them also in the ICustomGraphService interface!

Finally, let's move to the Program.cs file and, before the builder.Build() statement, add the following line of code:

builder.Services.AddScoped<ICustomGraphService, CustomGraphService>();

Now our CustomGraphService class can be easily consumed by every other class and Razor component in our application. Let's put this into action immediately by creating a new component to display the list of custom providers. Right click on the Pages folder in Solution Explorer, choose Add → Razor Component and give it a meaningful name, like LearningProviders.razor.

As first step, let's add the following code at the top of the component:

page "/learningProviders"
@inject ICustomGraphService graphService
@using Microsoft.Graph

The first one is used to enable routing, so that we can treat this component like a page that will be available through the /learningProviders endpoint of the web application. The second line is used to inject our custom Microsoft Graph wrapper in the component. The third one is required to access all the types included in the Microsoft Graph .NET SDK, which belongs to the Microsoft.Graph namespace.

Then, in the @code block, let's override the OnInitializedAsync() method, which gets called when the component is rendered:

@code {
    private IList<LearningProvider> providers;

    protected async override Task OnInitializedAsync()
    {
        providers = await graphService.GetLearningProvidersAsync();
    }
}

We call the GetLearningProvidersAsync() method we have previously created in our custom class, and we store the result into a variable called providers, which is a collection of LearningProvider objects. Then, we can iterate this collection to generate a table with the list of custom providers:

@if (providers != null)
{
    <table class="table">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                @*<th>Contents</th>*@
            </tr>
        </thead>
        <tbody>
            @foreach (var provider in providers)
            {
                <tr>
                    <td>@provider.Id</td>
                    <td>@provider.DisplayName</td>
                </tr>
            }
        </tbody>
    </table>
}
else
{
    <h3>Loading...</h3>
}

For each provider returned by the Microsoft Graph, we display its Id and DisplayName properties. To test our work, press F5 to launch the debugger and then point your browser to the URL https://localhost:7073/learningProviders. If you did everything correctly, you should see the custom provider we have created with Postman in the previous post:

 

blazor-learning-providers.png

Now that we have our learning provider, let's move forward and also display the list of contents available for this provider.

 

Listing the learning content

For this demo, we're going to enhance the component we have built to automatically display the list of contents which are available right below the custom provider table. The reason of this choice is that, if you remember, we can't have more than one custom learning provider per tenant, so it wouldn't make sense to give users the options to choose which learning provider they want to see: there will be only one.

 

Let's continue working on our CustomGraphService class to add a new method to retrieve the list of learning contents. Before doing that, however, we have some additional work to do. If you remember the learnings from the previous posts, one of the challenges you must deal with when you work with Viva Learning is that you must use different AAD permissions based on the scenario: delegated permissions to work with learning providers, application permissions to work with learning content. The Microsoft Graph client that is injected by the Microsoft Identity Web library is authenticated with a token retrieved with the Authorization code flow: when we start the web app, we are asked to login with a work account from our Microsoft 365 tenant. This means that the token is valid to perform delegated operations, thus we were able to use it just fine to retrieve the list of learning providers.

When we want to get the list of learning content, instead, we must use a token which supports performing application operations. As such, we can't use the injected Microsoft Graph client, but we'll need to get a valid access token using the client credential flow and create a new Graph client based on it. As first step, let's create a method to perform the authentication against our AAD application but, this time, using the client credential flow:

using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;

public class CustomGraphService : ICustomGraphService
{
    private readonly IConfiguration configuration;
    private readonly GraphServiceClient delegatedClient;
    private readonly MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler;
    private GraphServiceClient? applicationClient;

    public CustomGraphService(IConfiguration configuration, GraphServiceClient delegatedClient, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
    {
        this.configuration = configuration;
        this.delegatedClient = delegatedClient;
        this.consentHandler = consentHandler;
    }

    public async Task AcquireAccessTokenAsync()
    {
        var scopes = new string[] { ".default" };

        var aadConfig = configuration.GetSection("AzureAd");
        var client = ConfidentialClientApplicationBuilder.Create(aadConfig["ClientId"])
            .WithTenantId(aadConfig["TenantId"])
            .WithClientSecret(aadConfig["ClientSecret"]).Build();

        var token = await client.AcquireTokenForClient(scopes).ExecuteAsync();

        var authProvider = new DelegateAuthenticationProvider(async (request) =>
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token.AccessToken);
        });

        applicationClient = new GraphServiceClient(authProvider);
    }
}

Inside the AcquireAccessTokenAsync() method, we use the ConfidentialClientApplicationBuilder class included in the Microsoft.Identity namespace, which allows us to authenticate to our AAD application using the client credentials flow. We set it up by using the following methods:

  • Create(), which requires the Client Id of our AAD app.
  • WithTenantId(), which requires the Tenant Id of our AAD app.
  • WithClientSecret, which requires the Client Secret of our AAD app.

All the information have already been stored in the appsettings.json file when we have connected our application to the Microsoft Identity platform. As such, we can just retrieve the section called AzureAd from the configuration file, using the configuration object. Since this object is registered inside the DI container, we just need to add the IConfiguration interface as parameter of the public constructor of our CustomGraphService class. As the last step, we call the Build() method to create our authentication client.

Finally, we can use the AcquireTokenForClient() method with the scopes we need, followed by the ExecuteAsync() method, to effectively retrieve the access token we need to interact with the Microsoft Graph using application permissions. Once we have it, we can use it to create a DelegateAuthenticationProvider object, which we can pass as parameter to create a new GraphServiceClient object.

If we did everything properly, now our CustomGraphService class should offer two different GraphServiceClient objects: one called delegatedClient, which supports delegated permissions and that we can use to work with learning providers; one called applicationClient, which supports application permissions and that we can use to work with learning content.

Thanks to this second client implementation, now it's quite easy to add a new method to our CustomGraphService class that we can use to get the list of learning contents for a given learning provider:

public async Task<IList<LearningContent>?> GetLearningContentAsync(string id)
{
    var response = await applicationClient.EmployeeExperience.LearningProviders[id].LearningContents.Request().GetAsync();
    return response.CurrentPage;
}

Now, we can go back to our Razor component and add a couple of additional statements in the OnInitializedAsync() method:

@code {
    private IList<LearningProvider> providers;
    private IList<LearningContent> contents;

    protected async override Task OnInitializedAsync()
    {
        await graphService.AcquireAccessTokenAsync();
        providers = await graphService.GetLearningProvidersAsync();
        if (providers != null)
        {
            contents = await graphService.GetLearningContentAsync(providers?.FirstOrDefault().Id);
        }
    }
}

First, we are calling our new AcquireAccessTokenAsync() to make sure to get the proper access token before performing any operation. Then, after getting the list of providers, we call the new GetLearningContentAsync() method to get the list of contents for the first provider in the list (which will be also the only available one). The list of contents is stored in the contents variable, which is a collection of LearningContent objects. We use it in a similar way we did for the learning providers to build a table which displays the list:

@if (contents != null)
{
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Url</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var content in contents)
            {
                <tr>
                    <td>@content.Title</td>
                    <td>@content.ContentWebUrl</td>
                </tr>
            }
        </tbody>
    </table>

}
else
{
    <h3>Loading...</h3>
}

For each learning content, we display its Title and ContentWebUrl properties. If you have implemented everything properly, when you press F5 and you go to the https://localhost:7073/learningProviders page, you should see, below the customer provider, the articles from this blog we have added in the previous post with Postman:

 

blazor-learning-contents.png

 

Let's move now to the last step of our demo: providing a form to add new learning content.

 

Adding the learning content

Unfortunately, at the time of writing, we can't use the applicationClient object we have created in the previous section, due to the way the API works. If you remember what we have learned in the previous post, this API has a couple of specific requirements:

 

The Microsoft Graph .NET SDK, unfortunately, doesn't support this scenario. Additionally, the learning content request we have previously built using the applicationClient.EmployeeExperience.LearningProviders[id].LearningContents.Request() method, supports only two methods: GetAsync(), which is mapped with a GET request, and AddAsync(), which is mapped with a POST request. We don't have any UpdateAsync() method, which would be mapped with the PATCH request we need.

But don't worry, we can easily support our requirements by manually building our request using the standard HttpClient class in .NET.

First, let's change a bit our AcquireAccessTokenAsync() method so that we can store the access token as a global property of the CustomGraphService class:

public class CustomGraphService : ICustomGraphService
{
   private string? accessToken;

   public async Task AcquireAccessTokenAsync()
   {
       var scopes = new string[] { ".default" };

       var aadConfig = configuration.GetSection("AzureAd");
       var client = ConfidentialClientApplicationBuilder.Create(aadConfig["ClientId"])
           .WithTenantId(aadConfig["TenantId"])
           .WithClientSecret(aadConfig["ClientSecret"]).Build();

       var token = await client.AcquireTokenForClient(scopes).ExecuteAsync();
       accessToken = token.AccessToken;

       //  initialization of the application Graph client
   }
}

Now we can use the token to create a custom authenticated HttpClient and submit the proper request to the Microsoft Graph:

public async Task AddLearningContent(string providerId, string contentId, string title, string contentUrl, string language)
{
    string baseUrl = applicationClient.EmployeeExperience.LearningProviders.RequestUrl;
            
    string url = $"{baseUrl}/{providerId}/learningContents(externalId='{contentId}')";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    LearningContentModel content = new()
    {
        Title = title,
        ExternalId = contentId,
        ContentWebUrl = contentUrl,
        LanguageTag= language
    };

    JsonContent jsonContent = JsonContent.Create(content);

    var result = await client.PatchAsync(url, jsonContent);
}

First, we leverage the standard Microsoft Graph client to retrieve the base URL to work with the Viva Learning APIs. We use it to build the full URL we need to submit the request, by including the id of the learning provider and the external id. Then we create a new HttpClient instance, and we use the AuthenticationHeaderValue object with the access token to define the default Authorization header. Now the HttpClient object can make authenticated requests to the Microsoft Graph. The body of the request must be defined in JSON, so we create a new LearningContentModel object and we turn it into a JSON object using the JsonContent.Create() method. Finally, we submit the request using the PatchAsync() method, which is the equivalent of performing a PATCH operation.

There's only one caveat to keep in mind. As you can see, to create the JSON payload, we're using a LearningContentModel class, instead of the LearningContent one which is part of the Microsoft Graph .NET SDK. The reason is that this class contains a lot of extra properties that the standard Microsoft Graph .NET client can parse in the right way, but if we use it with a custom client (like in our case) it will generate an invalid request. As such, I've created a class called LearningContentModel which exposes only the properties I need:

public class LearningContentModel
{
    [Required]
    public string? ExternalId { get; set; }

    [Required]
    public string? Title { get; set; }

    [Required]
    public string? ContentWebUrl { get; set; }

    [Required]
    public string? LanguageTag { get; set; }
}

This is a quite simple implementation which exposes only the minimum set of properties which are required to create learning content. Of course, you can customize it to add any extra property you might need.

Now we have everything we need in terms of business logic. We need, however, the Razor component which will act as a submission form. Right click on the Pages folder and choose Add → Razor component. Give it a meaningful name, like NewLearningContent.razor and click Add.

Let's start to define the header of the file:

@page "/newLearningContent/{learningProviderId}"
@inject ICustomGraphService graphService;

We define the route of the page, this time however, by supporting a parameter. We're going to pass to the component, through the URL, the id of the learning provider we're going to create the content for. Then, we inject our usual CustomGraphService object. Now let's look at the code section:

@code {
    [Parameter]
    public string? LearningProviderId { get; set; }

    public LearningContentModel learningContentModel = new();

    public async Task HandleSubmit()
    {
        if (learningContentModel != null)
        {
            await graphService.AddLearningContent(LearningProviderId, learningContentModel.ExternalId, learningContentModel.Title, learningContentModel.ContentWebUrl, learningContentModel.LanguageTag);
        }
    }
}

The LearningProviderId property is decorated with the [Parameter] attribute, which means that it will be automatically injected with the value coming from the URL. We also define a new property of the type LearningContentModel: we're going to use it to build the input form. Finally, we define a method called HandleSubmit(), which is going to be triggered when the user clicks on the Submit button of the form. The method simply takes care of calling the AddLearningContent() method we have previously defined in the CustomGraphService class. When the user fills out the form, the learningContentModel object includes all the information filled in by the user. As such, we just pass its properties as inputs for the method.

Now that we have all the logic in place, we can build our input form using the EditForm component in Blazor:

<EditForm Model="@learningContentModel" OnValidSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <div>External Id</div>
        <div><InputText id="externalId" @bind-Value="learningContentModel.ExternalId" /></div>
    </p>
    <p>
        <div>Title</div>
        <div><InputText id="title" @bind-Value="learningContentModel.Title" /></div>
    </p>

    <p>
        <div>Content URL</div>
        <div><InputText id="contentUrl" @bind-Value="learningContentModel.ContentWebUrl" /></div>
    </p>

    <p>
        <div>Language</div>
        <div><InputText id="longLogoLight" @bind-Value="learningContentModel.LanguageTag" /></div>
    </p>

    <p>
        <button type="submit">Create</button>
    </p>

</EditForm>

The EditForm component requires two properties:

  • Model is the object which holds the model that will be filled with the data coming from the form. It's the learningContentModel property we have defined in the @code section.
  • OnValidSubmit, which is the method to invoke when the form is submitted. HandleSubmit is the name of the handler we have created in the @code section.

Inside the EditForm component, we have the freedom to define the various fields as we prefer. The only requirement is to use the @bind-value property to connect the field with the equivalent property in the model. We also need a button with submit as type, which will trigger the method defined in the OnValidSubmit property.

This is it. If you want to test this form, you can invoke the URL https://localhost:7073/newLearningContent/ with the id of your custom learning provider (for example, https://localhost:7073/newLearningContentBis/303da5b7-dce3-4998-a76f-7a18849fc697).

 

blazor-new-content.png

 

If you want to give the user an easy option to create new content, you can customize the LearningProviders.razor component and:

  1. Inject the NavigationManager object into the component, which is provided by Blazor to manage navigation across pages:

    @inject NavigationManager navigationManager
    
  2. Add a new button in the component:

    <button @onclick="CreateNewLearningContent">Add new content</button>
    
  3. Implement the CreateNewLearningContent method to perform the navigation:

    public void CreateNewLearningContent()
    {
        navigationManager.NavigateTo($"/newLearningContent/{providers?.FirstOrDefault().Id}");
    }
    

When the user presses the button, they will be automatically redirected to the newLearningContent page with, as an extra parameter, the id of the learning provider.

 

Wrapping up

It was quite a long journey, but now we have a fully working web client that we can use to manage our custom learning content in Viva Insights. In this post, we have focused on the various nuances you must keep in mind when you build such a solution, like the requirement of using different authentication types for the Microsoft Graph client to manage the different permissions required to work with the Viva Learning APIs.

 

You can find the full solution on GitHub. Before using it, remember to change the configuration in the appsettings.json file with the information about your app registration on Azure AD.

In the same solution, you will find also another project which uses the same classes and APIs to support another common scenarios with learning providers: content syncing. In this scenario, you don't manually add custom learning content, but you sync it from another source, like an Excel file or a Web API. The project on GitHub includes an Azure Function that reads a CSV file stored on Azure Storage and uses it to import a list of contents in the custom learning provider.

 

Happy coding!

Co-Authors
Version history
Last update:
‎Nov 29 2022 08:45 AM
Updated by: