May 8

Written by: Michael Washington
5/8/2019 10:57 AM  RssIcon

image

In this article we will create a Database Driven module for Oqtane, the new web application framework that runs in Microsoft Blazor. There are two methods to create custom modules in Oqtane, inline, and as a separate Visual Studio project. This article will cover creating a module inline.

image

Microsoft Blazor, a new web framework for .NET Core that lets you build interactive web UIs using C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.

Oqtane was created by Shaun Walker and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology. It is a modular framework offering a fully dynamic page compositing model, multi-site support, designer friendly templates ( skins ), and extensibility via third party modules.

Note: At this point Oqtane offers a minimum of desired functionality and is not recommended for production usage. The expectation is that Oqtane will rapidly evolve as a community driven open source project. At this point in time we do not promise any upgrade path from one version to the next, and developers should expect breaking changes as the framework stabilizes.

Getting Started

image

See the article Creating a Hello World Module For Blazor Oqtane for instructions on getting started with Microsoft’s Blazor and Oqtane.

image

For this article, we will create a To Do List module based on the Microsoft Build your first Blazor app tutorial.

 

Create the Data Layer: Add The Table

image

Open Oqtane in Visual Studio 2019 Preview (or higher).

image

In the Server project, right-click on the .mdf file in the Data folder and select Open.

image

This will open the Oqtane database in the Server Explorer window.

image

Right-click on the Tables folder and select New Query.

image

Enter the following script and click the Execute button:

 

CREATE TABLE [dbo].[ToDoItem] (
    [ToDoItemId] INT            IDENTITY (1, 1) NOT NULL,
    [ModuleId]   INT            NOT NULL,
    [Title]    NVARCHAR (4000) NOT NULL,
    [IsDone]    bit NOT NULL,
    CONSTRAINT [PK_ToDoItem] PRIMARY KEY CLUSTERED ([ToDoItemId] ASC),
    CONSTRAINT [FK_ToDoItem_Module] FOREIGN KEY ([ModuleId]) REFERENCES [dbo].[Module] ([ModuleId]) ON DELETE CASCADE
);

 

image

Close the script window (no need to save), and refresh the Tables folder…

image

The ToDoItem table will display.

image

It is very important that at this point we right-click on the database and select Close Connection.

 

Create the Data Layer: Add The Repository Code

image

In Visual Studio, return to the Solution Explorer window.

We need to create a class that will be shared between the Client UI and server-side code that will manage the To Do items.

In the Shared project, add a class called ToDoItemInfo.cs with the following code:

 

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Oqtane.Shared.Modules.ToDoItem.Models
{
    [Table("ToDoItem")]
    public class ToDoItemInfo
    {
        [Key]
        public int ToDoItemId { get; set; }
        public int ModuleId { get; set; }
        public string Title { get; set; }
        public bool IsDone { get; set; }
    }
}

 

image

We now need to add the repository code that will allow the custom module to communicate with the database.

Right-click on the Modules folder in the Server project and create a folder named ToDoItem.

image

Add a folder named Repository, and in that folder, add the following files:

 

IToDoItemRepository.cs

 

using System.Collections.Generic;
using Oqtane.Shared.Modules.ToDoItem.Models;
namespace Oqtane.Server.Modules.ToDoItem.Repository
{
    public interface IToDoItemRepository
    {
        IEnumerable<ToDoItemInfo> GetToDoItems();
        void AddToDoItem(ToDoItemInfo ToDoItem);
        void UpdateToDoItem(ToDoItemInfo ToDoItem);
        ToDoItemInfo GetToDoItem(int ToDoItemId);
        void DeleteToDoItem(int ToDoItemId);
    }
}

 

ToDoItemContext.cs

 

using Microsoft.EntityFrameworkCore;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Modules;
using Oqtane.Shared.Modules.ToDoItem.Models;
namespace Oqtane.Server.Modules.ToDoItem.Repository
{
    public class ToDoItemContext : ContextBase, IService
    {
        public virtual DbSet<ToDoItemInfo> ToDoItem { get; set; }
        public ToDoItemContext(ITenantRepository TenantRepository):base(TenantRepository)
        {
            // ContextBase handles multi-tenant database connections
        }
    }
}

 

ToDoItemRepository.cs

 

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using Oqtane.Modules;
using Oqtane.Shared.Modules.ToDoItem.Models;
namespace Oqtane.Server.Modules.ToDoItem.Repository
{
    public class ToDoItemRepository : IToDoItemRepository, IService
    {
        private readonly ToDoItemContext db;
        public ToDoItemRepository(ToDoItemContext context)
        {
            db = context;
        }
        public IEnumerable<ToDoItemInfo> GetToDoItems()
        {
            try
            {
                return db.ToDoItem.ToList();
            }
            catch
            {
                throw;
            }
        }
        public void AddToDoItem(ToDoItemInfo ToDoItem)
        {
            try
            {
                db.ToDoItem.Add(ToDoItem);
                db.SaveChanges();
            }
            catch
            {
                throw;
            }
        }
        public void UpdateToDoItem(ToDoItemInfo ToDoItem)
        {
            try
            {
                db.Entry(ToDoItem).State = EntityState.Modified;
                db.SaveChanges();
            }
            catch
            {
                throw;
            }
        }
        public ToDoItemInfo GetToDoItem(int ToDoItemId)
        {
            try
            {
                ToDoItemInfo ToDoItem = db.ToDoItem.Find(ToDoItemId);
                return ToDoItem;
            }
            catch
            {
                throw;
            }
        }
        public void DeleteToDoItem(int ToDoItemId)
        {
            try
            {
                ToDoItemInfo ToDoItem = db.ToDoItem.Find(ToDoItemId);
                db.ToDoItem.Remove(ToDoItem);
                db.SaveChanges();
            }
            catch
            {
                throw;
            }
        }
    }
}

 

A few things to note:

 

  • At the time of this writing we are waiting for the Blazor team to create additional functionality to handle authorization and authentication. When it is available, some of that code would go here.
  • This code would always run server-side. Even when running Oqtane in client-mode.
  • This code would not directly communicate with the Client UI layer, the controller code (created next), will do that.

 

 

Create the Data Layer: Add The Controller

image

We will now create the controller that will communicate with the Client UI layer (that will be created in the next section).

Add a folder named Controllers, and under that folder add a file called ToDoItemController.cs using the following code:

 

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Oqtane.Shared.Modules.ToDoItem.Models;
using Oqtane.Server.Modules.ToDoItem.Repository;
namespace Oqtane.Server.Modules.ToDoItem.Controllers
{
    [Route("{site}/api/[controller]")]
    public class ToDoItemController : Controller
    {
        private IToDoItemRepository toDoItem;
        public ToDoItemController(IToDoItemRepository ToDoItem)
        {
            toDoItem = ToDoItem;
        }
        // GET: api/<controller>
        [HttpGet]
        public IEnumerable<ToDoItemInfo> Get()
        {
            return toDoItem.GetToDoItems();
        }
        // GET api/<controller>/5
        [HttpGet("{id}")]
        public ToDoItemInfo Get(int id)
        {
            return toDoItem.GetToDoItem(id);
        }
        // POST api/<controller>
        [HttpPost]
        public void Post([FromBody] ToDoItemInfo HtmlText)
        {
            if (ModelState.IsValid)
                toDoItem.AddToDoItem(HtmlText);
        }
        // PUT api/<controller>/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] ToDoItemInfo ToDoItem)
        {
            if (ModelState.IsValid)
                toDoItem.UpdateToDoItem(ToDoItem);
        }
        // DELETE api/<controller>/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
            toDoItem.DeleteToDoItem(id);
        }
    }
}

 

A few things to note:

 

  • At the time of this writing we are waiting for the Blazor team to create additional functionality to handle authorization and authentication. When it is available, some of that code would go here.
  • This code would always run server-side. Even when running Oqtane in client-mode.
  • This code will directly communicate with the Client UI layer
  • If Oqtane were running only in server-side mode, you would not need this controller. Code in the Client UI could call the Repository code (created earlier) directly.

 

 

Create The User Interface : Service Interface

image

In he Client project, Add a folder in the Modules folder named ToDoItem.

Add a folder under that folder named Services.

Add the following files to that folder:

 

IToDoItemService.cs

 

using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Shared.Modules.ToDoItem.Models;
namespace Oqtane.Client.Modules.ToDoItem.Services
{
    public interface IToDoItemService 
    {
        Task<List<ToDoItemInfo>> GetToDoItemsAsync(int ModuleId);
        Task AddToDoItemAsync(ToDoItemInfo ToDoItem);
        Task UpdateToDoItemAsync(ToDoItemInfo ToDoItem);
        Task DeleteToDoItemAsync(int ToDoItemId);
    }
}

 

 

ToDoItemService.cs

 

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Oqtane.Services;
using Oqtane.Shared.Modules.ToDoItem.Models;
namespace Oqtane.Client.Modules.ToDoItem.Services
{
    public class ToDoItemService : ServiceBase, IToDoItemService
    {
        private readonly HttpClient http;
        private readonly string apiurl;
        public ToDoItemService(HttpClient http, IUriHelper urihelper)
        {
            this.http = http;
            apiurl = CreateApiUrl(urihelper.GetAbsoluteUri(), "ToDoItem");
        }
        public async Task<List<ToDoItemInfo>> GetToDoItemsAsync(int ModuleId)
        {
            List<ToDoItemInfo> ToDoItem = 
                await http.GetJsonAsync<List<ToDoItemInfo>>(apiurl);
            ToDoItem = ToDoItem
                .Where(item => item.ModuleId == ModuleId)
                .ToList();
            return ToDoItem;
        }
        public async Task AddToDoItemAsync(ToDoItemInfo ToDoItem)
        {
            await http.PostJsonAsync(apiurl, ToDoItem);
        }
        public async Task UpdateToDoItemAsync(ToDoItemInfo ToDoItem)
        {
            await http.PutJsonAsync(
                apiurl + "/" + ToDoItem.ToDoItemId.ToString(), 
                ToDoItem);
        }
        public async Task DeleteToDoItemAsync(int ToDoItemId)
        {
            await http.DeleteAsync(
                apiurl + "/" + ToDoItemId.ToString());
        }
    }
}

 

This code just allows us to keep the remaining code clean by handling all the calls to the data layer in a single place. 

 

Create The User Interface : The View

 

image

We will now create the first page of the custom module.

This will be the first code that will run when the module is invoked in Oqtane.

Create a file named Index.razor (right-click on the ToDoItem folder and select Add then New Item… choose the Text File template, but name the file Index.razor), using the following code:

 

@using Oqtane.Client.Modules.ToDoItem.Services
@using Oqtane.Modules
@using Oqtane.Shared.Modules.ToDoItem.Models
@using System.Net.Http;
@using Oqtane.Client.Modules.Controls
@inherits ModuleBase
@inject HttpClient http
@inject IUriHelper UriHelper
<ul style="list-style-type:none;">
    @foreach (var todo in ToDoItems)
    {
        <li>
            <input type="checkbox" bind="@todo.IsDone" />
            @todo.Title
        </li>
    }
</ul>
<br /><ActionLink Action="Edit" /><br /><br />
@functions {
    List<ToDoItemInfo> ToDoItems = 
        new List<ToDoItemInfo>();
    protected override async Task OnInitAsync()
    {
        ToDoItemService ToDoItemservice =
            new ToDoItemService(http, UriHelper);
        ToDoItems =
            await ToDoItemservice.GetToDoItemsAsync(
                ModuleState.ModuleId);
    }
}

 

Note that: ActionLink Action="Edit" in the code above means that the link will only show for users who have Edit permission to the module.

 

Create The User Interface : The Edit Interface

 

image

Oqtane allows the administrator to designate certain pages to be accessible only to certain roles (usually the administrator roles). We will create a separate page that will allow the To Do items to be edited.

Create a page called Edit.razor using the following code:

 

@using Microsoft.AspNetCore.Components.Routing
@using Oqtane.Modules
@using Oqtane.Client.Modules.ToDoItem.Services
@using Oqtane.Shared.Modules.ToDoItem.Models
@using System.Net.Http;
@using Oqtane.Client.Modules.Controls
@inherits ModuleBase
@inject IUriHelper UriHelper
@inject HttpClient http
<ul style="list-style-type:none;">
    @foreach (var todo in ToDoItems)
    {
        <li>
            <input type="checkbox" 
                   bind="@todo.IsDone" 
                   onclick="@(() => UpdateIsDone(todo))" /> 
            @todo.Title
            <button type="button" 
                    onclick="@(() => DeleteToDo(todo))">x
            </button>
        </li>
    }
</ul>
<input placeholder="Enter a new todo" bind="@newTodo" />
<button class="btn btn-success" onclick="@AddTodo">Add To Do</button>
@functions {
    // the SecurityAccessLevelEnum.Edit is what makes this configurable 
    // by the Oqtane administrator as an "Edit" control
    public override SecurityAccessLevelEnum SecurityAccessLevel
    {
        get { return SecurityAccessLevelEnum.Edit; }
    }
    // Oqtane uses this to display the tile of the custom module
    public override string Title
    {
        get { return "Edit To Do Items"; }
    }
    List<ToDoItemInfo> ToDoItems = new List<ToDoItemInfo>();
    private string newTodo;
    protected override async Task OnInitAsync()
    {
        await RefreshList();
    }
    private async Task RefreshList()
    {
        ToDoItemService ToDoItemservice = 
            new ToDoItemService(http, UriHelper);
        // This module can be placed in Oqtane multiple times
        // Each time it will have a different ModuleState.ModuleId
        ToDoItems = 
            await ToDoItemservice.GetToDoItemsAsync(
                ModuleState.ModuleId);
    }
    private async Task AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            ToDoItemService ToDoItemservice = 
                new ToDoItemService(http, UriHelper);
            ToDoItemInfo AddToDo = new ToDoItemInfo();
            AddToDo.ModuleId = ModuleState.ModuleId;
            AddToDo.Title = newTodo;
            AddToDo.IsDone = false;
            AddToDo.ModuleId = ModuleState.ModuleId;
            ToDoItems.Add(AddToDo);
            newTodo = string.Empty;
            await ToDoItemservice.AddToDoItemAsync(AddToDo);
            await RefreshList();
        }
    }
    private async Task DeleteToDo(ToDoItemInfo ToDoToDelete)
    {
        ToDoItemService ToDoItemservice = 
            new ToDoItemService(http, UriHelper);
        await ToDoItemservice.DeleteToDoItemAsync(
            ToDoToDelete.ToDoItemId);
        await RefreshList();
    }
    private async Task UpdateIsDone(ToDoItemInfo ToDoToUpdate)
    {
        ToDoToUpdate.IsDone = !ToDoToUpdate.IsDone;
        ToDoItemService ToDoItemservice = 
            new ToDoItemService(http, UriHelper);
        await ToDoItemservice.UpdateToDoItemAsync(ToDoToUpdate);
    }
}

 

The Module Manifest

image

We now need to create file that tells Oqtane what our module is so that it can allow it to be used.

Create a file called Module.cs and replace all the code with the following:

 

using Oqtane.Modules;
namespace Oqtane.Client.Modules.ToDoItem
{
    public class Module : IModule
    {
        public string Name { get { return "ToDoItem"; } }
        public string Description { get { return "Allows To Do Items to be created and tracked"; } }
        public string Version { get { return "1.0.0"; } }
        public string Owner { get { return ""; } }
        public string Url { get { return ""; } }
        public string Contact { get { return ""; } }
        public string License { get { return ""; } }
        public string Dependencies { get { return ""; } }
    }
}

 

Invoke The Module

 

image

Hit F5 to run it.

 

image

The site will display.

 

image

Click Login.

 

image

Login as host for the username and host for the password.

 

image

Note: At the time of this writing, there is a known issue in Blazor server side that wont detect the JavaScript interop method immediately, so for now, you will need to click on another page (for example page 3) then return to page 1.

 

image

Click the Admin link.

 

image

Select Page Management.

 

image

Select Add Page.

 

image

Create a new page.

 

image

Click the Back button.

 

image

Select the new page and then click on the hamburger menu.

 

image

The ToDoItem module will show up in the Module dropdown list (because of the Module.cs file).

Use the settings screen to add the module to the page.

 

image

The module will show on the page.

You can select the dropdown next to the module title to go to Settings.

 

image

This will allow you to set the security for this instance of the module.

Close the screen when done.

 

image

To add items, click the Edit button (this will only show for users in the security role that have Edit permission as configured in the previous step).

 

image

This will allow you to add and edit items.

 

image

Note that you can place multiple instances of the module throughout the site, with different security configurations, and their own list.

 

Links

Creating a Hello World Module For Blazor Oqtane

www.oqtane.org

Blazor.net

Get started with Blazor

Build your first Blazor app

Announcing Oqtane... a Modular Application Framework for Blazor!

Oqtane (GitHub)

Oqtane Custom Module Sample (GitHub)

Tags: Blazor , Oqtane
Categories:

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 
Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation