ReactiveUI+Telerik & MVVM with Blazor and MAUI

ReactiveUI+Telerik & MVVM with Blazor and MAUI

ReactiveUI is an advanced framework for building reactive, dynamic user interfaces in .NET applications. Built on the Reactive Extensions (Rx), it enables developers to compose asynchronous and event-based programs using observable sequences and LINQ-style query operators. This approach allows for the creation of responsive and efficient user interfaces, with a focus on managing side-effects and states in a declarative manner. ReactiveUI facilitates the development of complex user interfaces by simplifying the handling of intricate event-driven logic, which is common in modern applications. It's compatible with a variety of platforms, including Blazor and MAUI.

Telerik has advanced MVVM friendly UI controls for both Blazor and MAUI.


One of the key strengths of ReactiveUI is its integration of the Model-View-ViewModel (MVVM) pattern, enhancing the separation of concerns between the user interface and the business logic. This separation enables more testable and maintainable code. ReactiveUI provides powerful tools for binding data and commands, making it easier to synchronize the UI with underlying data models. It also encourages a more functional style of programming, reducing the reliance on mutable state and enabling developers to create more predictable and less error-prone code. By leveraging the capabilities of Rx, ReactiveUI allows developers to efficiently handle complex sequences of events, manage asynchronous operations, and create highly interactive user experiences.

In Blazor the Views are written in Razor and in MAUI they are written in XAML. In the following POC we have managed to share the ViewModels between Blazor and MAUI to maximize reusability and consistency between the platforms.

The Architecture


Data.Core

This package holds the Entity, RepositoryResultSet (used for paging), Entity Framework Core 7 Data Context, Unit of Work and Repository Interfaces. Eventually there will be interfaces for Entity specific operations, but for now we just expose basic CRUD operations.

public abstract class Entity<TKey>
{
    public abstract TKey PrimaryKey { get; set; }
    public DateTime? CreateDate { get; set; } = DateTime.UtcNow;
    public DateTime? UpdateDate { get; set; } = DateTime.UtcNow;
    public virtual string? CreatedByUserId { get; set; } = null!;
    public virtual string? UpdatedByUserId { get; set; } = null!;
    public virtual User? CreatedByUser { get; set; } = null!;
    public virtual User? UpdatedByUser { get; set; } = null!;
    public bool IsDeleted { get; set; }
}
public class RepositoryResultSet<TEntity, TKey>
    where TEntity : Entity<TKey>, new()
{
    public IEnumerable<TEntity> Entities { get; set; } = null!;
    public int? Count { get; set; }
    public int? PageSize { get; set; }
    public int? Page { get; set; }


}
public struct Pager
{
    public int Size { get; set; }
    public int Page { get; set; }
}

 public interface IContextFactory
 {
     TranscendV2Context CreateContext();
     Task<ClaimsPrincipal?> GetPrincipal();
 }
 public static class ClaimsPrincipalExtentension
 {
     public static string? GetUserId(this ClaimsPrincipal? principal)
     {
         return principal?.Claims.FirstOrDefault(p => p.Type == "https://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
     }
 }
 public class TranscendUnitOfWork : UnitOfWork
 {
     public TranscendV2Context Context { get; }
     public TranscendUnitOfWork(IContextFactory factory)
     {
         Context = factory.CreateContext();
     }
     public override Task Save(CancellationToken token = default)
     {
         return Context.SaveChangesAsync(token);
     }
     protected override void Dispose(bool disposing)
     {
         if (disposing)
             Context.Dispose();
         base.Dispose(disposing);
     }
 }
 public interface IRepository<TUnitOfWork>
     where TUnitOfWork : UnitOfWork
 {
     TUnitOfWork CreateUnitOfWork();
 }
 public interface IIRepository<TUnitOfWork, in TEntity, TKey> : IRepository<TUnitOfWork>
     where TEntity : Entity<TKey>, new()
     where TUnitOfWork : UnitOfWork
 {
     Task Delete(TKey id, TUnitOfWork? work = null, CancellationToken token = default);
     Task Delete(TEntity entity, TUnitOfWork? work = null, CancellationToken token = default);
     Task Add(TEntity entity, TUnitOfWork? work = null, CancellationToken token = default);
     
 }
 public struct EntityProperty
 {
     public string Name { get; }
     public bool IsCollection { get; }
     public EntityProperty(string name, bool isCollection = false)
     {
         Name = name;
         IsCollection = isCollection;
     }
 }
 public interface IRepository<TUnitOfWork, TEntity, TKey> : IIRepository<TUnitOfWork, TEntity, TKey>
     where TEntity : Entity<TKey>, new()
     where TUnitOfWork : UnitOfWork
 {
     Task<TEntity> Update(TEntity entity, 
         TUnitOfWork? work = null, CancellationToken token = default);
     Task<RepositoryResultSet<TEntity, TKey>> Get(TUnitOfWork? work = null,
         Pager? page = null,
         Expression<Func<TEntity, bool>>? filter = null,
         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
         IEnumerable<EntityProperty>? properites = null,
         CancellationToken token = default);
     Task<int> Count(TUnitOfWork? work = null,
         Expression<Func<TEntity, bool>>? filter = null,
         CancellationToken token = default);
     Task<TEntity?> GetByID(TKey key, TUnitOfWork? work = null,
         IEnumerable<EntityProperty>? properites = null, CancellationToken token = default);

 }        

An example Entity can be found here:

public partial class Person : Entity<Guid>
{
    public override Guid PrimaryKey { get => Id; set => Id = value; }
    public Guid Id { get; set; }

    public string FirstName { get; set; } = null!;

    public string? LastName { get; set; }

    public string? MiddlName { get; set; }
    public override string? CreatedByUserId
    {
        get => base.CreatedByUserId; set
        {
            base.CreatedByUserId = value;
            if (IdNavigation != null)
                IdNavigation.CreatedByUserId = value;
        }
    }
    public override string? UpdatedByUserId
    {
        get => base.UpdatedByUserId; set
        {
            base.UpdatedByUserId = value;
            if (IdNavigation != null)
                IdNavigation.UpdatedByUserId = value;
        }
    }

    public virtual ICollection<CommercialContactPerson> CommercialContactPeople { get; set; } = new List<CommercialContactPerson>();

    public virtual Contact IdNavigation { get; set; } = null!;

    public virtual ICollection<PersonAccount> PersonAccounts { get; set; } = new List<PersonAccount>();

}        

Data

The Data package contains the implementation for Data.Core's interfaces and will be called by the Business package injected into the ViewModels (for Blazor) and the Web API's Controllers.

 public class ContextFactory : IContextFactory
 {
     protected DbContextOptions<TranscendV2Context> Config { get; }
     protected IServiceProvider ServiceProvider { get; }
     public ContextFactory(DbContextOptions<TranscendV2Context> config, IServiceProvider provider)
     {
         Config = config;
         ServiceProvider = provider;
     }

     public TranscendV2Context CreateContext()
     {
         return new TranscendV2Context(Config);
     }
     public async Task<ClaimsPrincipal?> GetPrincipal()
     {
         var authState = ServiceProvider.GetService<AuthenticationStateProvider>();
         bool isBlazor = authState != null;
         if (authState != null)
         {
             try
             {
                 var state = await authState.GetAuthenticationStateAsync();
                 return state.User;
             }
             catch
             {
                 isBlazor = false;
             }
         }
         if(!isBlazor)
         {
             var httpContext = ServiceProvider.GetService<IHttpContextAccessor>();
             if (httpContext != null)
             {
                 return httpContext.HttpContext.User;
             }
             else
                 return Thread.CurrentPrincipal as ClaimsPrincipal;
         }
         return null;
     }
 }
 
 public class Repository<TEntity, TKey> : IRepository<TranscendUnitOfWork, TEntity, TKey>
    where TEntity : Entity<TKey>, new()
 {
     public virtual TranscendUnitOfWork CreateUnitOfWork()
     {
         return new TranscendUnitOfWork(ContextFactory);
     }
     protected IContextFactory ContextFactory { get; private set; }
     public Repository(IContextFactory contextFactory)
     {
         ContextFactory = contextFactory;
     }
     protected async Task Use(Func<TranscendUnitOfWork, CancellationToken, Task> worker,
         TranscendUnitOfWork? work = null, CancellationToken token = default,
         bool saveChanges = false)
     {
         bool hasWork = work != null;
         work ??= new TranscendUnitOfWork(ContextFactory);
         try
         {
             await worker(work, token);
         }
         finally
         {
             if (!hasWork)
             {
                 if (saveChanges)
                     await work.Save(token);
                 work.Dispose();
             }
         }
     }
     public virtual Task Delete(TKey key, TranscendUnitOfWork? work = null, CancellationToken token = default)
     {
         return Use(async (w, t) =>
         {
             TEntity? entity = await w.Context.FindAsync<TEntity>(key, token);
             if (entity != null)
             {
                 await Delete(entity, w, t);
             }

         }, work, token, true);

     }
     public virtual Task Delete(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
     {
         return Use(async (w, t) =>
         {
             var user = await ContextFactory.GetPrincipal();
             entity.UpdateDate = DateTime.UtcNow;
             entity.UpdatedByUserId = user?.GetUserId();
             entity.IsDeleted = true;
         }, work, token, true);
     }
     protected virtual async Task HydrateResultsSet(RepositoryResultSet<TEntity, TKey> results,
         IQueryable<TEntity> query,
         TranscendUnitOfWork w,
         CancellationToken t,
         Pager? page = null,
         Expression<Func<TEntity, bool>>? filter = null,
         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
         IEnumerable<EntityProperty>? properites = null)
     {
         if (filter != null)
         {
             query = query.Where(filter);
         }
         if (properites != null)
         {
             foreach (var propExp in properites.Select(e => e.Name))
                 query = query.Include(propExp);
         }
         if (page != null)
         {
             int skip = page.Value.Size * (page.Value.Page - 1);
             int take = page.Value.Size;
             results.PageSize = page.Value.Size;
             results.Page = page.Value.Page;
             results.Count = await query.CountAsync(t);
             if (orderBy != null)
                 results.Entities = await orderBy(query).Skip(skip).Take(take).ToArrayAsync(t);
             else
                 results.Entities = await query.Skip(skip).Take(take).ToArrayAsync(t);
         }
         else if (orderBy != null)
             results.Entities = await orderBy(query).ToArrayAsync(t);
         else
             results.Entities = await query.ToArrayAsync(t);
     }
     public virtual async Task<RepositoryResultSet<TEntity, TKey>> Get(TranscendUnitOfWork? work = null,
         Pager? page = null,
         Expression<Func<TEntity, bool>>? filter = null,
         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
         IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
     {
         RepositoryResultSet<TEntity, TKey> results = new RepositoryResultSet<TEntity, TKey>();
         bool hasWork = work != null;
         work ??= new TranscendUnitOfWork(ContextFactory);
         try
         {
             await Use(async (w, t) =>
             {
                 IQueryable<TEntity> query = w.Context.Set<TEntity>();
                 await HydrateResultsSet(results, query, w, t, page, filter, orderBy, properites);
             }, work, token);
         }
         finally
         {
             if (!hasWork)
                 work.Dispose();
         }
         return results;
     }

     public virtual async Task<TEntity?> GetByID(TKey key, TranscendUnitOfWork? work = null, IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
     {
         TEntity? entity = null;
         await Use(async (w, t) =>
         {
             entity = await w.Context.FindAsync<TEntity>(key, t);
             if (entity != null && properites != null)
                 foreach (var prop in properites)
                 {
                     if (prop.IsCollection)
                         await w.Context.Entry(entity).Collection(prop.Name).LoadAsync(t);
                     else
                         await w.Context.Entry(entity).Reference(prop.Name).LoadAsync(t);
                 }
         }, work, token);
         return entity;
     }

     public virtual Task Add(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
     {
         return Use(async (w, t) =>
         {
             var user = await ContextFactory.GetPrincipal();
             entity.CreateDate = DateTime.UtcNow;
             entity.UpdateDate = DateTime.UtcNow;
             entity.CreatedByUserId = user?.GetUserId();
             entity.UpdatedByUserId = entity.CreatedByUserId;
             await w.Context.AddAsync(entity, t);
         }, work, token, true);
     }

     public async virtual Task<TEntity> Update(TEntity entity, 
         TranscendUnitOfWork? work = null, CancellationToken token = default)
     {
         await Use(async (w, t) =>
         {
             var user = await ContextFactory.GetPrincipal();
             entity.UpdateDate = DateTime.UtcNow;
             entity.UpdatedByUserId = user?.GetUserId();
             w.Context.Attach(entity);
             w.Context.Update(entity);
         }, work, token, true);
         return entity;
     }

     public virtual async Task<int> Count(TranscendUnitOfWork? work = null,
         Expression<Func<TEntity, bool>>? filter = null,
         CancellationToken token = default)
     {
         int count = 0;
         await Use(async (w, t) =>
         {
             IQueryable<TEntity> query = w.Context.Set<TEntity>();
             if (filter != null)
                 query = query.Where(filter);

             count = await query.CountAsync(t);
         }, work, token);
         return count;
     }

 }        

Business.Core

This package exposes the interfaces used by the ViewModels and Web API controllers to perform CRUD operations, eventually there will be extensions for Entity specific operations.

 public interface IBusinessRepositoryFacade<TUoW>
 {
     TUoW? CreateUnitOfWork();
 }
 public interface IOBusinessRepositoryFacade<out TEntity, TKey, TUoW> : IBusinessRepositoryFacade<TUoW>
     where TEntity : Entity<TKey>, new()
     where TUoW : UnitOfWork
 {
 
 }
 public interface IIBusinessRepositoryFacade<in TEntity, TKey, TUoW> : IBusinessRepositoryFacade<TUoW>
    where TEntity : Entity<TKey>, new()
     where TUoW : UnitOfWork
 {
     Task Delete(TKey id, TUoW? work = null, CancellationToken token = default);
     Task Delete(TEntity entity, TUoW? work = null, CancellationToken token = default);
     
 }
 public interface IBusinessRepositoryFacade<TEntity, TKey, TUoW> : 
     IOBusinessRepositoryFacade<TEntity, TKey, TUoW>, 
     IIBusinessRepositoryFacade<TEntity, TKey, TUoW>
     where TEntity : Entity<TKey>, new()
     where TUoW: UnitOfWork
 {
     Task<RepositoryResultSet<TEntity, TKey>> Get(TUoW? work = null,
         Pager? page = null,
         Expression<Func<TEntity, bool>>? filter = null,
         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
         IEnumerable<EntityProperty>? properites = null,
         CancellationToken token = default);
     Task<int> Count(TUoW? work = null,
         Expression<Func<TEntity, bool>>? filter = null,
         CancellationToken token = default);
     Task<TEntity?> GetByID(TKey key, TUoW? work = null,
         IEnumerable<EntityProperty>? properites = null, CancellationToken token = default);
     Task<TEntity> Add(TEntity entity, TUoW? work = null, CancellationToken token = default);
     Task<TEntity> Update(TEntity entity,
         TUoW? work = null, CancellationToken token = default);
 }        

Business

The Business package is not referenced by MAUI, which will use REST operations to communicate with a Web API Business Layer that will reference this package.

public abstract class BusinessRepositoryFacade : IBusinessRepositoryFacade<TranscendUnitOfWork>
{
    protected IRepository<TranscendUnitOfWork> RepositoryRaw { get; }
    protected IRulesEngine RulesEngine { get; }
    protected ILogger Logger { get; }
    protected BusinessRepositoryFacade(IRepository<TranscendUnitOfWork> repository, IRulesEngine rulesEngine, ILogger logger)
    {
        RepositoryRaw = repository;
        RulesEngine = rulesEngine;
        Logger = logger;
    }

    public virtual TranscendUnitOfWork CreateUnitOfWork()
    {
        return RepositoryRaw.CreateUnitOfWork();
    }
}
public abstract class BusinessRepositoryFacade<TEntity, TKey> : BusinessRepositoryFacade,
    IOBusinessRepositoryFacade<TEntity, TKey, TranscendUnitOfWork>, IIBusinessRepositoryFacade<TEntity, TKey, TranscendUnitOfWork>
    where TEntity : Entity<TKey>, new()
{
    protected IRepository<TranscendUnitOfWork, TEntity, TKey> RepositoryDefault
    {
        get => (IRepository<TranscendUnitOfWork, TEntity, TKey>)RepositoryRaw;
    }
    protected BusinessRepositoryFacade(IRepository<TranscendUnitOfWork, TEntity, TKey> repository, 
        IRulesEngine rulesEngine, ILogger<TEntity> logger) 
        :base(repository, rulesEngine, logger) 
    {
        
    }
    

    public virtual async Task Delete(TKey id, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        try
        {
            var entity = await this.RepositoryDefault.GetByID(id, work, token: token);
            if (entity != null)
                await Delete(entity, work, token);
        }
        catch(Exception ex)
        {
            Logger.LogError(ex, ex.Message);
            throw;
        }
    }

    public virtual async Task Delete(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        try
        {
            if (RulesEngine.ContainsWorkflowOperation<TEntity>(out string workflowName))
            {
                var rules = await RulesEngine.ExecuteAllRulesAsync(workflowName, entity);
                if (!rules.TrueForAll(r => r.IsSuccess))
                    throw new InvalidOperationException();
            }
            await this.RepositoryDefault.Delete(entity, work, token);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, ex.Message);
            throw;
        }
    }

   
}
public class BusinessRepositoryFacade<TEntity, TKey, TRepository> : BusinessRepositoryFacade<TEntity, TKey>, IBusinessRepositoryFacade<TEntity, TKey, TranscendUnitOfWork>
    where TEntity : Entity<TKey>, new()
    where TRepository : IRepository<TranscendUnitOfWork, TEntity, TKey>
{
    protected TRepository Repository { get => (TRepository)RepositoryRaw; }
    public BusinessRepositoryFacade(TRepository repository, IRulesEngine rulesEngine, ILogger<TEntity> logger) : 
        base(repository, rulesEngine, logger)
    {
    }

    public virtual Task<int> Count(TranscendUnitOfWork? work = null, Expression<Func<TEntity, bool>>? filter = null, CancellationToken token = default)
    {
        return Repository.Count(work, filter, token);
    }

    public virtual Task<RepositoryResultSet<TEntity, TKey>> Get(TranscendUnitOfWork? work = null, Pager? page = null, Expression<Func<TEntity, bool>>? filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null, IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
    {
        return Repository.Get(work, page, filter, orderBy, properites, token);
    }

    public virtual Task<TEntity?> GetByID(TKey key, TranscendUnitOfWork? work = null, IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
    {
        return Repository.GetByID(key, work, properites, token);
    }
    public virtual async Task<TEntity> Update(TEntity entity,
        TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        try
        {
            if (RulesEngine.ContainsWorkflowOperation<TEntity>(out string workflowName))
            {
                var rules = await RulesEngine.ExecuteAllRulesAsync(workflowName, entity);
                if (!rules.TrueForAll(r => r.IsSuccess))
                    throw new InvalidOperationException();
            }
            await this.Repository.Update(entity, work, token);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, ex.Message);
            throw;
        }
        return entity;
    }
    public virtual async Task<TEntity> Add(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        try
        {
            if (RulesEngine.ContainsWorkflowOperation<TEntity>(out string workflowName))
            {
                var rules = await RulesEngine.ExecuteAllRulesAsync(workflowName, entity);
                if (!rules.TrueForAll(r => r.IsSuccess))
                    throw new InvalidOperationException();
            }
            await this.Repository.Add(entity, work, token);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex, ex.Message);
            throw;
        }
        return entity;
    }
}        

Business.REST

This package implements the Business.Core interfaces to perform REST operations. This package is used by MAUI

public interface IHttpClientFactory
{
    Task<HttpClient> Create(CancellationToken token = default);
}
public struct OrderBy
{
    public string ColumnName { get; set; }
    public bool IsDsc { get; set; }
}
public class BusinessHttpClientFactory : IHttpClientFactory
{
    protected Uri BaseUri { get; }
    protected IPublicClientApplication ClientApplication { get; }
    protected string ApiScope { get; }
    public BusinessHttpClientFactory(IConfiguration config, IPublicClientApplication clientApplication)
    {
        BaseUri = new Uri(config["NFC:APIBaseUri"] ?? throw new InvalidDataException());
        ClientApplication = clientApplication;
        ApiScope = config["MSGraphApi:Scopes"] ?? throw new InvalidDataException();
    }
    public async Task<HttpClient> Create(CancellationToken token = default)
    {
        HttpClient client = new HttpClient();
        client.BaseAddress = BaseUri;
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var accounts = (await ClientApplication.GetAccountsAsync()).ToList();
        var result = await ClientApplication.AcquireTokenSilent(new string[] { ApiScope }, accounts.First()).ExecuteAsync();
        if (string.IsNullOrWhiteSpace(result.AccessToken))
            throw new InvalidProgramException();
        client.DefaultRequestHeaders.Authorization =
                        new AuthenticationHeaderValue("Bearer", result.AccessToken);
        return client;
    }
}
public class BusinessFacadeClient<TEntity, TKey> :
    IBusinessRepositoryFacade<TEntity, TKey, TranscendUnitOfWork>
    where TEntity : Entity<TKey>, new()
{
    protected IHttpClientFactory ClientFactory { get; }
    public BusinessFacadeClient(IHttpClientFactory factory) 
    {
        ClientFactory = factory;
    }
    public async Task<TEntity> Add(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            var url = $"{repositoryName}/";
            using (var response = await client.PostAsJsonAsync(url, entity, token))
            {
                if (response.IsSuccessStatusCode)
                    entity = (await response.Content.ReadFromJsonAsync<TEntity>(cancellationToken: token)) ?? throw new InvalidDataException();
            }
        }
        return entity;
    }

    public async Task<int> Count(TranscendUnitOfWork? work = null, Expression<Func<TEntity, bool>>? filter = null, CancellationToken token = default)
    {
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            var url = $"{repositoryName}/count";
            using (var response = await client.GetAsync(url, token))
            {
                if (response.IsSuccessStatusCode)
                    return int.Parse((await response.Content.ReadAsStringAsync(cancellationToken: token)) ?? throw new InvalidDataException());
            }
        }
        return 0;
    }

    public TranscendUnitOfWork? CreateUnitOfWork()
    {
        return null;
    }

    public async Task Delete(TKey id, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            var url = $"{repositoryName}/{id}";
            using var resp = await client.DeleteAsync(url, token);
        }
    }

    public Task Delete(TEntity entity, TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        return Delete(entity.PrimaryKey, work, token);
    }
    private static string SerializeFilterExpression<TE>(Expression<Func<TE, bool>> filter)
    {
        var serializer = new ExpressionSerializer(new Serialize.Linq.Serializers.BinarySerializer());
        return Convert.ToBase64String(serializer.SerializeBinary(filter));
    }
    public async Task<RepositoryResultSet<TEntity, TKey>> Get(TranscendUnitOfWork? work = null, Pager? page = null, Expression<Func<TEntity, bool>>? filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null, IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
    {
        RepositoryResultSet<TEntity, TKey>? result = null;
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            if (page == null)
                throw new NotImplementedException();
            var url = $"{repositoryName}/results/{page.Value.Page}/size={page.Value.Size}";
            if (orderBy != null)
                url += "/sort=" + string.Join(",", OrderByClauseExtractor.ExtractOrderByClauses(orderBy(Enumerable.Empty<TEntity>().AsQueryable()).Expression).Where(c => c != null));
            if (properites != null)
                url += "/props=" + string.Join(",", properites.Select(c => c.Name + ":" + (c.IsCollection ? "col" : "ref")));
            if (filter != null)
                url += "/filter=" + SerializeFilterExpression(filter);
            using (HttpResponseMessage response = await client.GetAsync(url, token))
            {
                if (response.IsSuccessStatusCode)
                {
                    result = await response.Content.ReadFromJsonAsync<RepositoryResultSet<TEntity, TKey>>(cancellationToken: token);
                }
            }
        }
        return result;
    }

    public async Task<TEntity?> GetByID(TKey key, TranscendUnitOfWork? work = null, IEnumerable<EntityProperty>? properites = null, CancellationToken token = default)
    {
        TEntity? entity = null;
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            var url = $"{repositoryName}/{key}";
            if (properites != null)
                url += "/props=" + string.Join(",", properites.Select(c => c.Name + ":" + (c.IsCollection ? "col" : "ref")));
            using (var response = await client.GetAsync(url, token))
            {
                if (response.IsSuccessStatusCode)
                    entity = await response.Content.ReadFromJsonAsync<TEntity>(cancellationToken: token);
            }
        }
        return entity;
    }

    public async Task<TEntity> Update(TEntity entity,
        TranscendUnitOfWork? work = null, CancellationToken token = default)
    {
        using (var client = await ClientFactory.Create())
        {
            var repositoryName = typeof(TEntity).Name.ToLower();
            var url = $"{repositoryName}/";
            using (var response = await client.PutAsJsonAsync(url, entity, token))
            {
                if (response.IsSuccessStatusCode)
                    entity = (await response.Content.ReadFromJsonAsync<TEntity>(cancellationToken: token)) ?? throw new InvalidDataException();
            }
        }
        return entity;
    }
}
public static class OrderByClauseExtractor
{
    public static string[] ExtractOrderByClauses(Expression expression)
    {
        var visitor = new OrderByVisitor();
        visitor.Visit(expression);
        visitor.IsSecondHit = true;
        visitor.Visit(expression);
        visitor.OrderByClauses.Reverse();
        return visitor.OrderByClauses.ToArray();
    }
}

public class OrderByVisitor : ExpressionVisitor
{
    private readonly Stack<string> PropertyStack = new Stack<string>();
    public readonly List<string> OrderByClauses = new List<string>();
    public bool IsSecondHit { get; set; }
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        string? currentOrder;
        switch (node.Method.Name)
        {
            case "ThenBy":
                currentOrder = "asc";
                break;
            case "OrderBy":
                currentOrder = "asc";
                break;
            case "ThenByDescending":
                currentOrder = "dsc";
                break;
            case "OrderByDescending":
                currentOrder = "dsc";
                break;
            default: currentOrder = null; break;
        }
        if (IsSecondHit)
        {
            if (PropertyStack.TryPop(out string? p))
            {
                OrderByClauses.Add($"{p}:{currentOrder}");
            }

        }
        foreach (var argument in node.Arguments)
        {
            Visit(argument);
        }

        return node;
    }

    protected override Expression VisitUnary(UnaryExpression node)
    {
        if (!IsSecondHit)
            PropertyStack.Push((node.ToString().Split("=>")[1].Split('.')[1]));

        return node;
    }
}        

ViewModels

The ViewModels project is shared between Razor Views in the Web project and XAML Views in the MAUI project. Lets look at the PeopleViewModel as an example.

public class PeopleViewModel : ReactiveObject
{
    public ReactiveCommand<LoadParameters<Person, Guid>?, RepositoryResultSet<Person, Guid>> Load { get; }
    protected IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork> Facade { get; }
    protected IBusinessRepositoryFacade<ContactType, short, TranscendUnitOfWork> ContactTypeFacade { get; }
    public ContactType? ContactType { get; protected set; }
    public ICommand Upsert { get; }
    public AddPersonViewModel AddViewModel { get; }
    public Action Reload { get; set; } = null!;

    public PeopleViewModel(
        IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork> businessFacade,
        IBusinessRepositoryFacade<ContactType, short, TranscendUnitOfWork> contactTypeFacade)
    {
        Facade = businessFacade;
        ContactTypeFacade = contactTypeFacade;
        Load = ReactiveCommand.CreateFromTask<LoadParameters<Person, Guid>?, RepositoryResultSet<Person, Guid>>(DoLoad);
        AddViewModel = new AddPersonViewModel(this, Facade);
        Upsert = ReactiveCommand.CreateFromTask<Person>(DoUpsert);
    }
    protected async Task DoUpsert(Person currency)
    {
        if (currency.Id != Guid.Empty)
            await Facade.Update(currency);
        else
            await Facade.Add(currency);
        Reload();
    }
    protected async Task<RepositoryResultSet<Person, Guid>> DoLoad(LoadParameters<Person, Guid>? loadParameters = null, CancellationToken token = default)
    {
        ContactType ??= (await ContactTypeFacade.Get(filter: q => q.Name == "Person")).Entities.Single();
        var results = await Facade.Get(page: loadParameters?.Pager, filter:
            loadParameters?.Filter, orderBy: loadParameters?.OrderBy, token: token);
        return results;
    }
}
public class AddPersonViewModel : ReactiveObject
{
    
    private bool isOpen = false;
    public bool IsOpen
    {
        get => isOpen;
        set => this.RaiseAndSetIfChanged(ref isOpen, value);
    }
    protected IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork> Facade { get; }
    public PeopleViewModel Parent { get; }
    private Person? data;
    public Person? Data
    {
        get => data;
        set => this.RaiseAndSetIfChanged(ref data, value);
    }
    public ICommand Add { get; }
    public ICommand Open { get; }
    public ICommand Cancel { get; }
    public Person CreateNew()
    {
        return new Person()
        {
            IdNavigation = new Contact()
            {
                ContactTypeId = Parent?.ContactType?.ContactTypeId ?? throw new InvalidDataException()
            }
        };
    }
    public AddPersonViewModel(PeopleViewModel parent, IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork> facade)
    {
        Facade = facade;
        Parent = parent;
        Add = ReactiveCommand.CreateFromTask(DoAdd);
        Open = ReactiveCommand.Create(() => { 
            IsOpen = true;
            Data = CreateNew();
        });
        Cancel = ReactiveCommand.Create(() =>
        {
            IsOpen = false;
            Data = CreateNew();
        });
    }

    public async Task DoAdd(CancellationToken token = default)
    {
        await Facade.Add(Data ?? throw new InvalidDataException() , token: token);
        Parent.Reload();
        Data = CreateNew();
        IsOpen = false;
    }
}        

Web

The Web project is a mixture of Blazor (Server Side) and Web API.

The corresponding Razor views to the ViewModels above are as followed:

@page "/contacts/people"
@using ReactiveUI.Blazor
@using NFC.Transcend.Client.ViewModels
@using Telerik.Blazor.Components
@using NFC.Transcend.Web.Helpers
@using NFC.Transcend.Data
@using NFC.Transcend.Data.Core
@inject NavigationManager Nav
@inherits ReactiveInjectableComponentBase<PeopleViewModel>
@attribute [Authorize(Roles = "Teller")]
@if (ViewModel != null)
{
    <h3>People <AddPersonView ViewModel="ViewModel.AddViewModel" /></h3>
    <TelerikGrid @ref="grdRef"
                 EditMode="Telerik.Blazor.GridEditMode.Inline"
                 OnUpdate="ViewModel.Upsert.BindEditCommand<Person, Guid>(this)"
                 TItem="Person" Sortable="true" FilterMode="Telerik.Blazor.GridFilterMode.FilterMenu"
                 OnRead="ViewModel.Load.BindReadCommand<Person, Guid>(this)">
        <GridColumns>
            <GridColumn Field="LastName" Title="Last Name" />
            <GridColumn Field="FirstName" Title="First Name" />
            <GridColumn Field="MiddlName" Title="Middle Name" />
            <GridColumn Field="Id" Title="Open">
                <Template>
                    <TelerikButton OnClick="() => RedirectToPerson(((Person)context).Id)">(...)</TelerikButton>
                </Template>
            </GridColumn>
        </GridColumns>
    </TelerikGrid>
}

@code {
    protected TelerikGrid<Person> grdRef;
    protected void Reload()
    {
        grdRef?.Rebind();
    }
    protected void RedirectToPerson(Guid id)
    {
        Nav.NavigateTo($"/contacts/people/{id}");
    }
    protected override async Task OnInitializedAsync()
    {
        if (ViewModel != null)
            ViewModel.Reload = Reload;
        await base.OnInitializedAsync();
    }
}        
@using ReactiveUI.Blazor
@using NFC.Transcend.Client.ViewModels
@using Telerik.Blazor.Components
@using NFC.Transcend.Web.Helpers
@using NFC.Transcend.Data
@inherits ReactiveComponentBase<AddPersonViewModel>
@if (ViewModel != null)
{
    <button class="btn btn-primary" @onclick="ViewModel.Open.BindCommand<MouseEventArgs>()"><i class="fa-solid fa-plus"></i></button>
    <TelerikDialog @bind-Visible="ViewModel.IsOpen" Title="Add Person">
        <DialogContent>
            <TelerikForm Model="ViewModel.Data" OnValidSubmit="ViewModel.Add.BindCommand<EditContext>()">
                <FormValidation>
                    <DataAnnotationsValidator />
                </FormValidation>
                <FormItems>
                    <FormItem Field="@nameof(Person.LastName)" LabelText="Last Name" />
                    <FormItem Field="@nameof(Person.FirstName)" LabelText="First Name" />
                    <FormItem Field="@nameof(Person.MiddlName)" LabelText="Middle Name" />
                </FormItems>
            </TelerikForm>
        </DialogContent>
        <DialogButtons>
            <TelerikButton OnClick="ViewModel.Cancel.BindCommand<MouseEventArgs>()">Cancel</TelerikButton>
        </DialogButtons>
    </TelerikDialog>

}
@code {

}
        


There were some customizations needed to support ICommand with Telerik:

 public static EventCallback<GridCommandEventArgs> BindEditCommand<TEntity, TKey>(this  ICommand command, object reciever)
 {
     return EventCallback.Factory.Create<GridCommandEventArgs>(reciever, args =>
     {
         command.Execute(args.Item);
     });
 }
 public static EventCallback<GridReadEventArgs> BindReadCommand<TEntity, TKey>(
     this ReactiveCommand<LoadParameters<TEntity, TKey>?, RepositoryResultSet<TEntity, TKey>> command, object reciever)
     where TEntity: Entity<TKey>, new()
 {
     return EventCallback.Factory.Create<GridReadEventArgs>(reciever, async args =>
     {
         LoadParameters<TEntity, TKey> parameters = new LoadParameters<TEntity, TKey>();
         if (args.Request.PageSize > 0)
             parameters.Pager = new Pager()
             {
                 Page = args.Request.Page,
                 Size = args.Request.PageSize
             };
         if (args.Request.Sorts.Count > 0)
         {
             parameters.OrderBy = args.Request.Sorts.ConvertSortDescriptors<TEntity>();
         }
         if (args.Request.Filters.Count > 0)
             parameters.Filter = args.Request.Filters.CombineFiltersIntoExpression<TEntity>();
         var result = await command.Execute(parameters).GetAwaiter();
         var data = result.Entities.ToArray();
         args.Data = data;
         args.Total = result.Count ??data.Length;
     });
 }        

Then the Web API controller base class is as followed:


 [Route("api/[controller]")]
 [ApiController]
 [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
 public abstract class BusinessController<TEntity, TKey, TRepository, TBusiness> : ControllerBase
     where TEntity : Entity<TKey>, new()
     where TRepository: IRepository<TranscendUnitOfWork, TEntity, TKey>
     where TBusiness: IBusinessRepositoryFacade<TEntity, TKey, TranscendUnitOfWork>

 {
     protected TBusiness Facade { get; }
     protected ILogger Logger { get; }
     public BusinessController(TBusiness business, ILogger<TEntity> logger)
     {
         Facade = business;
         Logger = logger;
     }
     [HttpGet("results/{page}/size={pageSize}")]
     [HttpGet("results/{page}/size={pageSize}/filter={filter}")]
     [HttpGet("results/{page}/size={pageSize}/sort={orderBy}")]
     [HttpGet("results/{page}/size={pageSize}/sort={orderBy}/filter={filter}")]
     [HttpGet("results/{page}/size={pageSize}/sort={orderBy}/props={includes}")]
     [HttpGet("results/{page}/size={pageSize}/sort={orderBy}/props={includes}/filter={filter}")]
     [HttpGet("results/{page}/size={pageSize}/props={includes}")]
     [HttpGet("results/{page}/size={pageSize}/props={includes}/filter={filter}")]
     public virtual async Task<RepositoryResultSet<TEntity, TKey>> Results(int page, int pageSize, string? orderBy = null, string? includes = null, string? filter = null, CancellationToken token = default)
     {

         using (var uow = Facade.CreateUnitOfWork())
         {
             int count = await Facade.Count(uow, token: token);
             Pager pager = new Pager()
             {
                 Page = page,
                 Size = pageSize
             };
             Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderClause = null;
             if (orderBy != null)
             {
                 var orderData = orderBy.Split(',').Select(x => x.Split(':')).Select(x => new { ColumnName = x[0], Dsc = x[1].ToLower() == "dsc" });
                 orderClause = query =>
                 {
                     IOrderedQueryable<TEntity> orderedQuery = null;
                     bool isFirst = true;
                     foreach (var descriptor in orderData)
                     {
                         if (isFirst)
                             orderedQuery = descriptor.Dsc
                                 ? OrderByDescending(query, descriptor.ColumnName)
                                 : OrderBy(query, descriptor.ColumnName);
                         else
                         {
                             orderedQuery = descriptor.Dsc
                                 ? ThenByDescending(orderedQuery, descriptor.ColumnName)
                                 : ThenBy(orderedQuery, descriptor.ColumnName);
                         }
                         isFirst = false;
                     }
                     return orderedQuery;
                 };

             }
             List<EntityProperty> props = new List<EntityProperty>();
             if (includes != null)
             {
                 foreach (var include in includes.Split(','))
                 {
                     var i = include.Split(':');
                     props.Add(new EntityProperty(i[0], i[1].ToLower() == "col"));
                 }
             }
             Expression<Func<TEntity, bool>>? f = null;
             if(filter != null)
             {
                 f = DeserializeFilterExpression<TEntity>(filter);
             }
             var data = await Facade.Get(uow, pager, properites:
                 includes != null ? props : null, filter: f,
                 orderBy: orderClause, token: token);
             return data;
         }

     }
     public static IOrderedQueryable<T> OrderBy<T>(
     IQueryable<T> source,
     string property)
     {
         return ApplyOrder<T>(source, property, "OrderBy");
     }

     public static IOrderedQueryable<T> OrderByDescending<T>(
         IQueryable<T> source,
         string property)
     {
         return ApplyOrder<T>(source, property, "OrderByDescending");
     }

     public static IOrderedQueryable<T> ThenBy<T>(
         IOrderedQueryable<T> source,
         string property)
     {
         return ApplyOrder<T>(source, property, "ThenBy");
     }

     public static IOrderedQueryable<T> ThenByDescending<T>(
         IOrderedQueryable<T> source,
         string property)
     {
         return ApplyOrder<T>(source, property, "ThenByDescending");
     }
     static IOrderedQueryable<T> ApplyOrder<T>(
     IQueryable<T> source,
     string property,
     string methodName)
     {
         string[] props = property.Split('.');
         Type type = typeof(T);
         ParameterExpression arg = Expression.Parameter(type, "x");
         Expression expr = arg;
         foreach (string prop in props)
         {
             // use reflection (not ComponentModel) to mirror LINQ
             PropertyInfo pi = type.GetProperty(prop);
             expr = Expression.Property(expr, pi);
             type = pi.PropertyType;
         }
         Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
         LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);

         object result = typeof(Queryable).GetMethods().Single(
                 method => method.Name == methodName
                         && method.IsGenericMethodDefinition
                         && method.GetGenericArguments().Length == 2
                         && method.GetParameters().Length == 2)
                 .MakeGenericMethod(typeof(T), type)
                 .Invoke(null, new object[] { source, lambda });
         return (IOrderedQueryable<T>)result;
     }
     private static Expression<Func<TE, bool>>? DeserializeFilterExpression<TE>(string serializedFilter)
     {
         var serializer = new ExpressionSerializer(new Serialize.Linq.Serializers.BinarySerializer());
         var exp = serializer.DeserializeBinary(Convert.FromBase64String(serializedFilter));
         return exp as Expression<Func<TE, bool>>;
     }
     [HttpGet("count")]
     [HttpGet("count/filter={filter}")]
     public virtual Task<int> Count(string? filter = null, CancellationToken token = default)
     {
         Expression<Func<TEntity, bool>>? f = null;
         if (filter != null)
         {
             f = DeserializeFilterExpression<TEntity>(filter);
         }
         return Facade.Count(filter: f, token: token);
     }
     [HttpGet("{id}")]
     [HttpGet("{id}/props={includes}")]
     public virtual Task<TEntity?> Get(TKey id, string? includes = null, CancellationToken token = default)
     {
         List<EntityProperty> props = new List<EntityProperty>();
         if (includes != null)
         {
             foreach (var include in includes.Split(','))
             {
                 var i = include.Split(':');
                 props.Add(new EntityProperty(i[0], i[1].ToLower() == "col"));
             }
         }
         return Facade.GetByID(id, properites: props, token: token);
     }
     [HttpPost]
     public virtual async Task<TEntity> Add([FromBody] TEntity entity, CancellationToken token = default)
     {
         await Facade.Add(entity, token: token);
         return entity;
     }
     [HttpPut]
     public virtual async Task<TEntity> Update([FromBody] TEntity entity, CancellationToken token = default)
     {
         await Facade.Update(entity, token: token);
         return entity;
     }
     [HttpDelete("{id}")]
     public virtual Task Delete(TKey id, CancellationToken token = default)
     {
         return Facade.Delete(id, token: token);
     }

 }
 public static class QueryableExtensions
 {
     public static Func<IQueryable<T>, IOrderedQueryable<T>> Compose<T>(
         this Func<IQueryable<T>, IOrderedQueryable<T>> first,
         Func<IQueryable<T>, IOrderedQueryable<T>> second)
     {
         return query => second(first(query));
     }
 }        

With the PersonController instantiated with the following:

 [RoleAuthorization("Admin")]
 public class PersonController : BusinessController<Person, Guid, IRepository<TranscendUnitOfWork, Person, Guid>,
     IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork>>
 {
     public PersonController(IBusinessRepositoryFacade<Person, Guid, TranscendUnitOfWork> business, ILogger<Person> logger) : base(business, logger)
     {
     }
 }        

To parse the Azure AD B2C roles Middleware and a custom AuthorizationAttribute are required:

public class RoleAuthorizationAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    readonly string[] _roles;

    public RoleAuthorizationAttribute(params string[] roles)
    {
        _roles = roles;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var isAuthenticated = context.HttpContext.User.Identity.IsAuthenticated;
        if (!isAuthenticated)
        {
            context.Result = new UnauthorizedResult();
            return;
        }

        var hasAllRequredClaims = _roles.All(r => context.HttpContext.User.IsInRole(r));
        if (!hasAllRequredClaims)
        {
            context.Result = new ForbidResult();
            return;
        }
    }
}
public class RolePopulationMiddleware
{
    private readonly RequestDelegate _next;
    public RolePopulationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.User.Identity != null && context.User.Identity.IsAuthenticated)
        {
            var identity = context.User.Identity as ClaimsIdentity;
            if (identity != null && identity.HasClaim(c => c.Type == "extension_Roles"))
            {
                var rolesClaim = context.User.FindFirst(c => c.Type == "extension_Roles") ?? throw new InvalidDataException();
                var roles = rolesClaim.Value.Split(',').Select(role => role.Trim());
                foreach ( var role in roles)
                {
                    identity.AddClaim(new Claim(ClaimTypes.Role, role));
                }
                
            }
        }
        await _next(context);
    }
}        

MAUI

MAUI uses the same ViewModels as Blazor and injects them with the REST package.


<?xml version="1.0" encoding="utf-8" ?>
<r:ReactiveContentPage x:TypeArguments="vm:PeopleViewModel" x:DataType="vm:PeopleViewModel" 
                       xmlns="https://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="NFC.Transcend.Client.MAUI.Views.PeopleView"
              xmlns:r="clr-namespace:ReactiveUI.Maui;assembly=ReactiveUI.Maui"
    xmlns:telerik="https://schemas.telerik.com/2022/xaml/maui"
   xmlns:toolkit="https://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:c="clr-namespace:NFC.Transcend.Client.MAUI.Converters;assembly=NFC.Transcend.Client.MAUI"
xmlns:vm="clr-namespace:NFC.Transcend.Client.ViewModels;assembly=NFC.Transcend.Client.ViewModels"
 xmlns:controls="clr-namespace:NFC.Transcend.Client.MAUI.Controls"
  xmlns:views="clr-namespace:NFC.Transcend.Client.MAUI.Views"
 xmlns:d="clr-namespace:NFC.Transcend.Data;assembly=NFC.Transcend.Data.Core"
                       xmlns:sys="clr-namespace:System;assembly=System.Runtime"
             Title="People">
    <VerticalStackLayout>
        <views:AddPersonView ViewModel="{Binding AddViewModel}" BindingContext="{Binding AddViewModel}"/>
        <controls:DataGrid x:TypeArguments="d:Person, sys:Guid" LoadCommand="{Binding Load}" Reload="{Binding Reload, Mode=OneWayToSource}">
            <controls:DataGrid.Columns>
                <telerik:DataGridTextColumn PropertyName="LastName" Name="Last Name" 
                                            CanUserSort="True" CanUserFilter="True"/>
                <telerik:DataGridTextColumn PropertyName="FirstName" Name="First Name" 
                            CanUserSort="True" CanUserFilter="True"/>
                <telerik:DataGridTextColumn PropertyName="MiddlName" Name="Middle Name" 
            CanUserSort="True" CanUserFilter="True"/>
            </controls:DataGrid.Columns>
        </controls:DataGrid>
    </VerticalStackLayout>
</r:ReactiveContentPage>        
<?xml version="1.0" encoding="utf-8" ?>
<r:ReactiveContentView x:TypeArguments="vm:AddPersonViewModel" x:DataType="vm:AddPersonViewModel" xmlns="https://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="https://schemas.microsoft.com/winfx/2009/xaml"
                           xmlns:r="clr-namespace:ReactiveUI.Maui;assembly=ReactiveUI.Maui"
    xmlns:telerik="https://schemas.telerik.com/2022/xaml/maui"
   xmlns:toolkit="https://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:c="clr-namespace:NFC.Transcend.Client.MAUI.Converters;assembly=NFC.Transcend.Client.MAUI"
xmlns:vm="clr-namespace:NFC.Transcend.Client.ViewModels;assembly=NFC.Transcend.Client.ViewModels"
 xmlns:controls="clr-namespace:NFC.Transcend.Client.MAUI.Controls"
  xmlns:views="clr-namespace:NFC.Transcend.Client.MAUI.Views"
 xmlns:d="clr-namespace:NFC.Transcend.Data;assembly=NFC.Transcend.Data.Core"  
             x:Class="NFC.Transcend.Client.MAUI.Views.AddPersonView">
    <HorizontalStackLayout>
        <telerik:RadButton Text="&#x2b;" FontFamily="FASolid" BackgroundColor="Blue" TextColor="White" FontSize="30" Command="{Binding Open}">

        </telerik:RadButton>
        <telerik:RadPopup.Popup>
            <telerik:RadPopup IsOpen="{Binding IsOpen}" Placement="Center">
                <VerticalStackLayout BackgroundColor="White" WidthRequest="300" HeightRequest="200">
                    <Label>Last Name</Label>
                    <Editor Text="{Binding Data.LastName}"/>
                    <Label>First Name</Label>
                    <Editor Text="{Binding Data.FirstName}"/>
                    <Label>Middle Name</Label>
                    <Editor Text="{Binding Data.MiddlName}"/>
                    <telerik:RadButton Text="Add" Command="{Binding Add}"/>
                </VerticalStackLayout>
            </telerik:RadPopup>
        </telerik:RadPopup.Popup>
    </HorizontalStackLayout>
</r:ReactiveContentView>
        

Out of the box the Telerik Grid does not support server side Paging, Filtering or Sorting so I created a DataGrid MAUI Control that wraps the Telerik Control.

public class DataGrid<TEntity, TKey> : ContentView
	where TEntity: Entity<TKey>, new()
{
	public int PageSize
	{
		get => (int)GetValue(PageSizeProperty); set => SetValue(PageSizeProperty, value);
	}
	public static readonly BindableProperty PageSizeProperty = BindableProperty.Create(nameof(PageSize), typeof(int), typeof(DataGrid<TEntity, TKey>), 25);
    public int CurrentPage
    {
        get => (int)GetValue(PageProperty); set => SetValue(PageProperty, value);
    }
    public static readonly BindableProperty PageProperty = BindableProperty.Create(nameof(Page), typeof(int), typeof(DataGrid<TEntity, TKey>), 1);
    public ReactiveCommand<LoadParameters<TEntity, TKey>, RepositoryResultSet<TEntity, TKey>> LoadCommand
    {
        get => GetValue(LoadCommandProperty) as ReactiveCommand<LoadParameters<TEntity, TKey>, RepositoryResultSet<TEntity, TKey>>;
        set => SetValue(LoadCommandProperty, value);
    }
    public static readonly BindableProperty LoadCommandProperty = BindableProperty.Create(nameof(LoadCommand), typeof(ReactiveCommand<LoadParameters<TEntity, TKey>, RepositoryResultSet<TEntity, TKey>>), typeof(DataGrid<TEntity, TKey>));
    protected RadDataGrid GridControl { get; }
    protected RadComboBox PageSelector { get; } = new RadComboBox();
    public DataGridColumnCollection Columns { get => GridControl.Columns; set {
            GridControl.Columns.Clear();
            foreach(var col in value)
                GridControl.Columns.Add(col);
        } }
    public static readonly BindableProperty ReloadProperty = BindableProperty.Create(nameof(Reload), typeof(Action), typeof(DataGrid<TEntity, TKey>));
    public Action Reload { get => GetValue(ReloadProperty) as Action; set => SetValue(ReloadProperty, value); }
    public DataGrid()
	{
        GridControl = new RadDataGrid();
        GridControl.FilterDescriptors.CollectionChanged += FilterDescriptors_CollectionChanged;
        GridControl.SortDescriptors.CollectionChanged += SortDescriptors_CollectionChanged;
        GridControl.Loaded += GridControl_Loaded;
        GridControl.AutoGenerateColumns = false;
        PageSelector.SelectionChanged += PageSelector_SelectionChanged;
        Reload = async () => await LoadData();
		Content = new VerticalStackLayout
		{
			Children = {
                GridControl,
                new HorizontalStackLayout()
                {
                    Children =
                    {
                        PageSelector
                    }
                }
            }
		};
	}

    private async void PageSelector_SelectionChanged(object sender, ComboBoxSelectionChangedEventArgs e)
    {
        var cmbo = (RadComboBox)sender;
        if (cmbo.SelectedItem == null || (cmbo.SelectedItem as int?) == CurrentPage)
            return;
        CurrentPage = (int)cmbo.SelectedItem;
        await LoadData();
    }

    private async void GridControl_Loaded(object sender, EventArgs e)
    {
        await LoadData();
    }

    private async void SortDescriptors_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        CurrentPage = 1;
        await LoadData();
    }

    private async void FilterDescriptors_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        CurrentPage = 1;
        await LoadData();
    }
    protected async Task LoadData()
    {
        
        if(LoadCommand != null)
        {
            var sort = ConvertSortDescriptors(GridControl.SortDescriptors);
            var filter = CombineFiltersIntoExpression(GridControl.FilterDescriptors);
            LoadParameters<TEntity, TKey> parms = new LoadParameters<TEntity, TKey>();
            parms.Pager = new Pager()
            {
                Page = CurrentPage,
                Size = PageSize
            };
            parms.OrderBy = sort;
            parms.Filter = filter;
            var result = await LoadCommand.Execute(parms).GetAwaiter();
            GridControl.ItemsSource = result.Entities;
            List<int> pages = new List<int>();
            int i = 0;
            while(i < Math.Ceiling(result.Count.Value/ (decimal)result.PageSize ))
            {
                pages.Add(i + 1);
                i++;
            }
            CurrentPage = result.Page ?? 1;
            PageSelector.ItemsSource = pages;
            PageSelector.SelectedItem = result.Page;
        }
    }
    public static Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> ConvertSortDescriptors(SortDescriptorCollection sortDescriptors)
    {
        if (sortDescriptors.Count == 0)
            return null;
        return query =>
        {
            IOrderedQueryable<TEntity> orderedQuery = null;
            bool isFirst = true;
            foreach(var descriptor in sortDescriptors.OfType<PropertySortDescriptor>())
            {
                if(isFirst)
                    orderedQuery = descriptor.SortOrder == SortOrder.Descending
                        ? OrderByDescending(query, descriptor.PropertyName)
                        : OrderBy(query, descriptor.PropertyName);
                else
                {
                    orderedQuery = descriptor.SortOrder == SortOrder.Descending
                        ? ThenByDescending(orderedQuery, descriptor.PropertyName)
                        : ThenBy(orderedQuery, descriptor.PropertyName);
                }
                isFirst = false;
            }
            return orderedQuery;
        };
    }
    public static IOrderedQueryable<T> OrderBy<T>(
    IQueryable<T> source,
    string property)
    {
        return ApplyOrder<T>(source, property, "OrderBy");
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(
        IQueryable<T> source,
        string property)
    {
        return ApplyOrder<T>(source, property, "OrderByDescending");
    }

    public static IOrderedQueryable<T> ThenBy<T>(
        IOrderedQueryable<T> source,
        string property)
    {
        return ApplyOrder<T>(source, property, "ThenBy");
    }

    public static IOrderedQueryable<T> ThenByDescending<T>(
        IOrderedQueryable<T> source,
        string property)
    {
        return ApplyOrder<T>(source, property, "ThenByDescending");
    }
    static IOrderedQueryable<T> ApplyOrder<T>(
    IQueryable<T> source, 
    string property, 
    string methodName) 
{
    string[] props = property.Split('.');
    Type type = typeof(T);
    ParameterExpression arg = Expression.Parameter(type, "x");
    Expression expr = arg;
    foreach(string prop in props) {
        // use reflection (not ComponentModel) to mirror LINQ
        PropertyInfo pi = type.GetProperty(prop);
        expr = Expression.Property(expr, pi);
        type = pi.PropertyType;
    }
    Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
    LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);

    object result = typeof(Queryable).GetMethods().Single(
            method => method.Name == methodName
                    && method.IsGenericMethodDefinition
                    && method.GetGenericArguments().Length == 2
                    && method.GetParameters().Length == 2)
            .MakeGenericMethod(typeof(T), type)
            .Invoke(null, new object[] {source, lambda});
    return (IOrderedQueryable<T>)result;
}
    public static Expression<Func<TEntity, bool>> CombineFiltersIntoExpression(FilterDescriptorCollection filterDescriptors)
    {

        if (filterDescriptors == null || !filterDescriptors.Any())
            return null; // Returns an expression that always true if no filters are provided

        var parameter = Expression.Parameter(typeof(TEntity), "entity");
        Expression combinedExpression = null;

        foreach (var filterDescriptor in filterDescriptors.OfType<TextFilterDescriptor>())
        {
            var member = Expression.PropertyOrField(parameter, filterDescriptor.PropertyName);
            var constant = Expression.Constant(filterDescriptor.Value, filterDescriptor.Value.GetType());

            Expression expression = filterDescriptor.Operator switch
            {
                TextOperator.EqualsTo => Expression.Equal(member, constant),
                TextOperator.Contains => Expression.Call(member, typeof(string).GetMethod("Contains", new[] { typeof(string) }), constant),
                TextOperator.DoesNotContain => Expression.Not(Expression.Call(member, typeof(string).GetMethod("Contains", new[] { typeof(string) }), constant)),
                TextOperator.EndsWith => Expression.Call(member, typeof(string).GetMethod("EndsWith", new[] { typeof(string) }), constant),
                TextOperator.DoesNotEqualTo => Expression.NotEqual(member, constant),
                TextOperator.IsNotEmpty => Expression.NotEqual(member, Expression.Constant(string.Empty)),
                TextOperator.IsEmpty => Expression.OrElse(Expression.Equal(member, Expression.Constant(null)), Expression.Equal(member, Expression.Constant(string.Empty))),
                TextOperator.StartsWith => Expression.Call(member, typeof(string).GetMethod("StartsWith", new[] { typeof(string) }), constant),
                _ => throw new NotImplementedException($"Operator {filterDescriptor.Operator} not implemented."),
            };

            combinedExpression = combinedExpression == null ? expression : Expression.AndAlso(combinedExpression, expression);
        }
        foreach (var filterDescriptor in filterDescriptors.OfType<NumericalFilterDescriptor>())
        {
            var member = Expression.PropertyOrField(parameter, filterDescriptor.PropertyName);
            var constant = Expression.Constant(filterDescriptor.Value, filterDescriptor.Value.GetType());

            Expression expression = filterDescriptor.Operator switch
            {
                NumericalOperator.IsGreaterThan => Expression.GreaterThan(member, constant),
                NumericalOperator.IsLessThan => Expression.LessThan(member, constant),
                NumericalOperator.EqualsTo => Expression.Equal(member, constant),
                NumericalOperator.IsGreaterThanOrEqualTo => Expression.GreaterThanOrEqual(member, constant),
                NumericalOperator.IsLessThanOrEqualTo => Expression.LessThanOrEqual(member, constant),
                NumericalOperator.DoesNotEqualTo => Expression.NotEqual(member, constant),
                _ => throw new NotImplementedException($"Operator {filterDescriptor.Operator} not implemented."),
            };

            combinedExpression = combinedExpression == null ? expression : Expression.AndAlso(combinedExpression, expression);
        }
        return Expression.Lambda<Func<TEntity, bool>>(combinedExpression, parameter);
    }
}
        

要查看或添加评论,请登录

Jason L. Lind的更多文章

  • Manifest Destiny of Cognition - A Libertarian Perspective

    Manifest Destiny of Cognition - A Libertarian Perspective

    The supreme art of war is to subdue the enemy without fighting -- Sun Tzu Men willingly believe what they wish --…

  • Coming to Terms with Suffering from Aspects of Mental Illness

    Coming to Terms with Suffering from Aspects of Mental Illness

    In 2007 I was diagnosed with Atypical Bi-Polar Type-1 with Severe Manic Episodes under the DSM IV. Great periods of…

    2 条评论
  • The Ultimate Top 25 Movies of All Time

    The Ultimate Top 25 Movies of All Time

    #1: Doctor Strangelove (1964) #2: The Manchurian Candidate (1962) #3: Memento (2000) #4: Casablanca (1942) #5: M (1931)…

  • An Introduction to Transformation Engineering

    An Introduction to Transformation Engineering

    I define a Transformation Engineer as "a change agent for an organization who drives innovation of the…

  • Zero Trust Architecture (ZTA) Overview

    Zero Trust Architecture (ZTA) Overview

    Trust not: verify always! – Zero Trust was first coined in late 2009 by John Kindervag, a principal analyst at…

  • UTHOUGHT: Cognitive Warfare Training for ROTC

    UTHOUGHT: Cognitive Warfare Training for ROTC

    Abstract The Mind is the Next Frontier to Protect and Defend & it is Already under Attack from our Adversaries (1) At…

  • A Cognitive (UN)Civil War

    A Cognitive (UN)Civil War

    What is Cognitive Warfare? Cognitive Warfare, for our purposes, is simply next-order Cyberwarfare, or “beyond the bits…

  • Subject Matter Turing Test: Bughouse (4 player chess)

    Subject Matter Turing Test: Bughouse (4 player chess)

    The Game The game is Bughouse, a four player chess derivative. Two players comprise a team and each play a chess board…

    1 条评论
  • Ideal Organizational Theory Redux

    Ideal Organizational Theory Redux

    Ideal organization theory formulates a methodology for not only what it takes to create artificial intelligence but…

  • Secure Cognitive Architecture

    Secure Cognitive Architecture

    Historically base security has had a focus of maintaining a tight perimeter with additional perimeters configured at…

社区洞察

其他会员也浏览了