65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET Core、JavaScript、PostgreSQL 和 ChartJs 构建动态仪表板 Web 应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2021年1月24日

CPOL

11分钟阅读

viewsIcon

39556

downloadIcon

813

使用 .NET MVC Core 创建一个漂亮的仪表板网页的步骤

引言

在专业领域,公司首席执行官或合格的管理人员希望能够快速访问所有关键数据点,以帮助他们进行分析、比较和做出恰当的决策。

仪表板是了解公司基本数据的全局视图的一种方式,这些仪表板的一些用例包括比较特定两年之间的净销售额、网站每月订阅者数量以及 Azure 租户的订阅销售数量。仪表板通常由图表和表格表示。

有许多 JavaScript 库可以帮助构建漂亮的图形可视化,其中最好的一个就是 ChartJs

通过本文,我们将构建一个漂亮的仪表板 Web 应用程序,用于显示一些关于订阅用户指标的信息。该应用程序将使用 C#、ASP.NET MVC Core、JavaScript 和 ChartJs 库构建。

阅读本文后,您将了解更多关于

  • 清晰架构
  • 使用代码优先方法的 Entity Framework Core
  • 依赖注入
  • PostgreSQL 数据库
  • ChartJs
  • 使用 PostMan 测试 API 端点

必备组件

要理解本文,您应该具备 ASP.NET MVC Core 和 JavaScript 的基本知识。

创建项目架构

在本教程中,我们将采用清晰架构原则从头开始开发我们的应用程序。
采用 清晰架构,我们的应用程序将在可维护性和可测试性方面获得很多好处,这得益于关注点分离,并且它不会侧重于特定框架或技术,而是侧重于领域逻辑。我建议您访问 此链接此链接 以获得完整定义并深入了解此类最佳实践。

我们的项目将分为四个部分

  • UI:由以下几部分组成
    • 交互器:它拦截演示层发送的请求,执行相关的场景,并将正确的结果返回给视图显示。对于我们的示例,它是一个 API 控制器。
    • 演示层:它构成了 GUI 部分,可以使用任何框架开发,例如 Angular、AngularJs、ASP.NET MVC Core 等。在我们的示例中,它将是一个 ASP.NET MVC Core 项目。
  • 应用程序逻辑:一组工作流(用例),用于实现我们的业务规则。它们的主要目的是接收来自控制器的请求模型,并将其转换为结果,然后将结果传递回视图。在我们的例子中,它将是一个 .NET Core 库项目。
  • 领域:引用我们业务逻辑的模型或实体的集合。它应该独立于框架。在我们的例子中,它将是一个 .NET Core 库项目。
  • 基础设施:它包含管理和收集来自数据库、服务、库或文件等外部数据源数据的方式。基础设施使用领域类与外部数据源进行交互并收集响应数据。对于我们的应用程序,它将包含用于与 PostgreSQL 数据库进行交互的存储库和配置。在我们的例子中,它将是一个 .NET Core 库项目。

下图显示了最终项目结构的快照

创建数据库

得益于使用 EF Framework,我们的应用程序将独立于数据库,我们可以接入任何类型的数据库,如 PostgreSQL、Oracle 或 SqlServer 数据库,我们只需要更改提供程序。

对于这个应用程序,我们将使用 PostgreSQL,首先我们需要将它安装在本地机器上并创建一个新的空数据库,为此

  • 此链接 下载并安装 pgadmin4。
  • 此链接 下载并安装 pgAgent。
  • 创建空数据库:我们需要启动 pgadmin 应用程序并按照以下方式创建一个新数据库

实现后端

I) 创建数据访问

DataAccess 是一组类和配置,可确保应用程序和数据库之间的对话。其主要职责是定义业务实体,执行 CRUD 操作,并将应用程序数据请求转换为数据库服务器已知的指令,反之亦然。

通信通过以下一项或多项技术或框架来确保:ADO.NET、EF、NHibernate 等,它们都有相同的目标,即简化和透明化应用程序与数据库之间的对话过程。

对于我们的应用程序,我们将使用 EF (Entity Framework),这是 .NET Core 项目中最流行的 ORM,它提供了许多优点,例如

  • 领域类和关系数据之间的映射。

  • 通过使用 Linq to Entities,在管理和收集数据库数据方面引入了更多的抽象。

  • 可以支持不同的关系数据库系统,如 PostgreSQL、Oracle 和 SqlServer。

  • 提供多种方法,如代码优先、数据库优先、模型优先。

  • 数据可以借助延迟加载机制按需加载。

  • 与 ADO.NET 等旧数据访问技术相比,EF ORM 通过映射过程使从数据库读写数据更加容易,用户将更多地关注如何开发业务逻辑,而不是构建查询。它节省了可观的开发时间。

  • 在本节中,我们将重点介绍如何创建模型与数据库实体之间的链接,使用 EF 代码优先方法创建数据库架构,并准备一个数据集用于我们的演示。

1) 创建实体和关系

我们的数据库架构将由以下实体组成

  • User:由名字、年龄、职业和性别表示。
  • Profession:用户可以拥有的职业,例如牙医、软件开发人员、教师等。

下面的类图将清楚地描述这些实体之间的关系以及每个表的属性列表

这些主要类将在 DashBoardWebApp.Domain 项目的 Entities 文件夹中创建

  • 创建 User
      public class User
        {
            public int? Id { get; set; }
            public string FirstName { get; set; }
            public int Age { get; set; }
            public string Gender { get; set; }
            public DateTime CreatedAt { get; set; }
            public int ProfessionId { get; set; }
            public Profession Profession { get; set; }        
        }
  • 创建 Profession
    public class Profession
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public List<User> Users { get; set; }
        }

2) 设置数据库

为确保应用程序模型和数据库实体之间的映射,我们必须遵循以下步骤

  • 首先,使用 Nuget 包管理器安装 EF Core 包和 PostgreSQL 提供程序。

  • 配置实体与关系数据之间的映射

    DashBoardWebApp.Infrastructure/Data/Config 中,我们定义了要映射的每个实体的约束和关系列表。

  • 创建 UserEntityConfiguration
     public class UserEntityConfiguration : IEntityTypeConfiguration<User>
        {
            public void Configure(EntityTypeBuilder<User> builder)
            {
                builder.ToTable("User");
    
                builder.Property(u => u.Id)
                .ValueGeneratedOnAdd()
                .HasColumnType("serial")
                .IsRequired();
    
                builder.HasKey(u => u.Id)
                .HasName("pk_user");
    
                builder.Property(u => u.FirstName).IsRequired();
                builder.Property(u => u.Age).IsRequired().HasDefaultValue(0);
                builder.Property(u => u.Gender).IsRequired().HasDefaultValue("Male");
                builder.Property(u => u.CreatedAt).IsRequired().HasDefaultValueSql
                                ("CURRENT_TIMESTAMP");
    
                builder.Property(u => u.ProfessionId).HasColumnType("int");
                builder.HasOne(u => u.Profession).WithMany(p => p.Users).HasForeignKey
                        (u => u.ProfessionId).HasConstraintName("fk_user_profession");
            }
        }
  • 创建 ProfessionEntityConfiguration

        public class ProfessionEntityConfiguration : IEntityTypeConfiguration<Profession>
        {
            public void Configure(EntityTypeBuilder<Profession> builder)
            {
                builder.ToTable("Profession");
    
                builder.HasKey(p => p.Id)
                  .HasName("pk_profession");
    
                builder.HasIndex(p => p.Name).IsUnique(true).HasDatabaseName
                                ("uc_profession_name");
                
                builder.HasMany(p => p.Users).WithOne(u => u.Profession);
            }
        }
  • 创建种子数据

    我们需要创建一个 ModelBuilderExtensions 类,通过添加初始化数据库数据的 seed 方法来扩展 ModelBuilder 的功能。此类将在 DashBoardWebApp.Infrastructure/Data/Config 中创建。

    public static class ModelBuilderExtensions
        {
            public static void Seed(this ModelBuilder modelBuilder)
            {
                List<Profession> professions = new List<Profession>()
                {
                     new Profession() { Id = 1, Name = "Software Developer"},
                     new Profession() { Id = 2, Name = "Dentist"},
                     new Profession() { Id = 3, Name = "Physician" }
                };
                
                modelBuilder.Entity<Profession>().HasData(
                  professions
                );
    
                List<User> users = new List<User>()
                {
                     new User() { Id=1, FirstName = "O.Nasri 1", Age = 30, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 01) },
                     new User() { Id=2, FirstName = "O.Nasri 2 ", Age = 31, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 02) },
                     new User() { Id=3, FirstName = "O.Nasri 3", Age = 32, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 02) },
                     new User() { Id=4, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 01, 04) },
                     new User() { Id=5, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2019, 02, 05) },
    
                     new User() { Id=6, FirstName = "Sonia 1", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 04, 01) } ,
                     new User() { Id=7, FirstName = "Sonia 2", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 04, 02) } ,
                     new User() { Id=8, FirstName = "Sonia 3", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 05, 03) } ,
                     new User() { Id=9, FirstName = "Sonia 4", Age = 20, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2019, 05, 04) } ,
               
                     new User() { Id=10, FirstName = "O.Nasri 1", Age = 30, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 01) },
                     new User() { Id=11, FirstName = "O.Nasri 2 ", Age = 31, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 02) },
                     new User() { Id=12, FirstName = "O.Nasri 3", Age = 32, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 02) },
                     new User() { Id=13, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 04) },
                     new User() { Id=14, FirstName = "O.Nasri 4", Age = 33, Gender = "Male", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 01, 05) },
    
                     new User() { Id=15, FirstName = "Thomas 1", Age = 41, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 01) } ,
                     new User() { Id=16, FirstName = "Thomas 2", Age = 42, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 02) } ,
                     new User() { Id=17, FirstName = "Thomas 3", Age = 43, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 03) } ,
                     new User() { Id=18, FirstName = "Thomas 4", Age = 44, Gender = "Male", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 03, 04) } ,
    
                     new User() { Id=19, FirstName = "Christophe 1", Age = 25, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 01) },
                     new User() { Id=20, FirstName = "Christophe 2", Age = 26, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 02) },
                     new User() { Id=21, FirstName = "Christophe 3", Age = 27, Gender = "Male", 
                                  ProfessionId = 3, CreatedAt = new DateTime(2020, 05, 03)},
    
                     new User() { Id=22,  FirstName = "Linda 1", Age = 18, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 01) },
                     new User() { Id=23,  FirstName = "Linda 2 ", Age = 19, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 02) },
                     new User() { Id=24, FirstName = "Linda 3", Age = 20, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 02) },
                     new User() { Id=25, FirstName = "Linda 4", Age = 21, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 04) },
                     new User() { Id=26, FirstName = "Linda 4", Age = 22, Gender = "Female", 
                                  ProfessionId = 1, CreatedAt = new DateTime(2020, 06, 05) },
    
                     new User() { Id=27, FirstName = "Dalida 1", Age = 40, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 06) } ,
                     new User() { Id=28, FirstName = "Dalida 2", Age = 41, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 07) } ,
                     new User() { Id=29, FirstName = "Dalida 3", Age = 42, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 08) } ,
                     new User() { Id=30, FirstName = "Dalida 4", Age = 43, Gender = "Female", 
                                  ProfessionId = 2, CreatedAt = new DateTime(2020, 09, 09) } ,
                };
    
                modelBuilder.Entity<User>().HasData(
                    users
               );
            }
        }
  • 使用 dbContext 创建映射

    public class BDDContext : DbContext
        {
            public BDDContext([NotNullAttribute] DbContextOptions options) : base(options)
            {
            }
    
            public DbSet<User> Users { get; set; }
            public DbSet<Profession> Professions { get; set; }
    
            #region Required
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.UseSerialColumns();
    
                modelBuilder.ApplyConfiguration<User>(new UserEntityConfiguration());
                modelBuilder.ApplyConfiguration<Profession>
                             (new ProfessionEntityConfiguration());
    
                modelBuilder.Seed();
            }
            #endregion
        }
  • 执行迁移过程

    在此步骤中,我们希望在对模型进行每次修改后创建或更新数据库结构,并在数据库为空时用一组数据填充数据库。所有这些都可以通过 EF 迁移工具 来完成。

    为此,我们需要修改 Program 类的内容如下

       public class Program
        {
            public static void Main(string[] args)
            {
                var host = CreateHostBuilder(args).Build();
    
                using (var scope = host.Services.CreateScope())
                {
                    var db = scope.ServiceProvider.GetRequiredService<BDDContext>();
                    db.Database.Migrate();
                }
    
                host.Run();
            }
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    });
        }

    并且使用 dotnet 迁移工具,我们可以在 Infrastructure 项目中运行以下命令

    dotnet ef migrations add InitialCreate --output-dir Migrations

    最后,当我们启动 Visual Studio 项目时,我们的模型和数据将在选定的数据库中创建。

    现在在这个层面,我们可以使用 Linq to Entities 来执行数据库查询和检索数据。

3) 创建存储库

此模式引入了更多关于查询和管理数据库数据的抽象。它被认为是 dataAccess 部分的一个主要入口点,并且包含各种方法,用于执行 CRUD 或更复杂的数据源操作。

每个存储库只能管理一个数据库实体,它将使用 dbcontextlinqToEntity 来查询映射对象(我们的业务实体)。

实现将放入 Infrastructure 项目,因为它依赖于外部源,并将通过接口和依赖注入 (DI) 暴露给其他项目。

  • 创建泛型存储库类

    DashBoardWebApp.Infrastructure/Data 文件夹中,我们创建一个 Repository 类。此类将包含所有特定存储库的所有常见数据访问方法,我们可以列出

    • Delete(TEntity entity):从数据库上下文中删除一个实体,此操作将在调用 context.savechanges() 后应用于数据库。
    • GetAll(Expression<Func<TEntity, bool>> filter = null, List<string> propertiesToInclude = null):返回所有满足 filter 参数传递条件的实体。返回的结果可以包含通过“propertiesToInclude”参数指定的任何关系,这要归功于 Entity Framework 的预加载。
    • Insert(TEntity entity):向数据库上下文添加新的实体数据,当我们调用 context.savechanges() 方法时,对象将在数据库中创建。
    • Update(TEntity entity):更新现有实体数据,当我们调用 context.savechanges() 方法时,对象将在数据库中更新。
    public class Repository<TEntity> where TEntity : class
        {
            internal BDDContext context;
            internal DbSet<TEntity> dbSet;
    
            public Repository(BDDContext context)
            {
                this.context = context;
                this.dbSet = context.Set<TEntity>();
            }
    
            /// <summary>
            /// remove entity if exists.
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Delete(TEntity entity)
            {
                this.dbSet.Remove(entity);
            }
    
            /// <summary>
            /// return all entities that match with condition passed by filter argument. 
            /// The result will include all specified relations specified by the 
            /// propertiesToInclude argument.
            /// </summary>
            /// <param name="filter">where condition</param>
            /// <param name="propertiesToInclude">list of relation can be eager loaded</param>
            /// <returns></returns>
            public virtual List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null, 
                   List<string> propertiesToInclude = null)
            {
                var query = this.dbSet.AsQueryable();
    
                if (propertiesToInclude != null && propertiesToInclude.Count > 0)
                {
                    propertiesToInclude.ForEach(p =>
                    {
                        query = query.Include(p);
                    });
                }
    
                if (filter != null)
                {
                    return query.Where(filter).ToList();
                }
                else
                {
                    return query.ToList();
                }
            }
    
            /// <summary>
            /// create a new entity
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Insert(TEntity entity)
            {
                this.dbSet.Add(entity);
            }
    
            /// <summary>
            /// update an existing entity. 
            /// </summary>
            /// <param name="entity"></param>
            public virtual void Update(TEntity entity)
            {
                this.dbSet.Update(entity);
            }
        }
  • 实现 UserRepository

    DashBoardWebApp.Domain/Repositories 文件夹中,创建 IUserRepository 接口以定义所需的数据访问方法列表,这些方法对于在应用程序逻辑项目中实现业务规则非常有用。这些方法包括

    • GetUsersByYear(int year):返回特定年份创建的所有用户。
    • GetAllCreatedUsersYears():返回所有创建用户的年份。此信息对于构建用户创建年份数据的过滤器很有用。
      public interface IUserRepository
        {
            List<User> GetUsersByYear(int year);
            List<int> GetAllCreatedUsersYears();
        }

    之后,我们在 DashBoardWebApp.Infrastructure/Data/Repositories 文件夹中创建 UserRepository.cs。此类应重用 Repository 类的通用方法并实现 IUserRepository 接口声明的方法。

    public class UserRepository : Repository<User>, IUserRepository
        {
            private readonly BDDContext _context;
    
            public UserRepository(BDDContext context) : base(context)
            {
                this._context = context;
            }
    
            public List<User> GetUsersByYear(int year)
            {
                Expression<Func<User, bool>> filterByYear = (u) => u.CreatedAt.Year == year;
    
                List<String> propertiesToInclude = new List<string>() { "Profession" };
                return base.GetAll(filterByYear, 
                       propertiesToInclude)?.OrderBy(u => u.CreatedAt).ToList();
            }
    
            public List<int> GetAllCreatedUsersYears()
            {
                return this.dbSet?.Select
                       (u => u.CreatedAt.Year).Distinct().OrderBy(y => y).ToList();
            }
        }

    完成存储库的实现后,我们可以使用它们来构建我们的应用程序逻辑?但是,在此之前,我们需要在 **依赖注入 (DI) 系统** 中声明它,方法是修改 Startup 类的 ConfigureServices 方法。

         public void ConfigureServices(IServiceCollection services)
            {
                services.AddControllersWithViews();
                services.AddDbContext<BDDContext>(
                  options => options.UseNpgsql("Host=localhost; user id=postgres; 
                             password=YOUR_PASSWORD; database=DashboardBDD"));
    
                services.AddScoped<IUserRepository, UserRepository>();
            }

II) 实现应用程序逻辑

我们要实现的用例是检索三种数据

  • 第一个是检索特定年份的订阅用户,按月份分组,这些数据可以显示在线图表组件中。

  • 第二个是检索特定年份的订阅用户,按职业分组,这些数据将显示在饼图中。

  • 第三个是检索特定年份的订阅用户,按年龄分组,这些数据将显示在饼图中。

为了进行实现,我们需要创建我们的视图模型类

  • DashBoardWebApp.Application/common/DTO 中创建 LineChartDataDTO 模型。

    它保存折线图内部点的 x、y 坐标。

       public class LineChartDataDTO
        {
            public DateTime X { get; set; }
            public decimal Y { get; set; }
    
            public LineChartDataDTO()
            {
    
            }
    
            public LineChartDataDTO(DateTime x, int y)
            {
                this.X = x;
                this.Y = y;
            }
        }
  • DashBoardWebApp.Application/common/DTO 中创建 PieChartDataDTO 模型。

    它保存饼图中切片的标签和百分比值。

    public class PieChartDataDTO
        {
            public string Label { get; set; }
            public decimal Value { get; set; }
    
            public PieChartDataDTO()
            {
    
            }
    
            public PieChartDataDTO(string label, decimal value)
            {
                Label = label;
    
                Value = Math.Round(value, 2);
            }
        }
  • DashBoardWebApp.Application/UseCases/DashBoard/DTO 中创建 DashBoardDTO 模型。

    此模型包含客户端所需的所有数据,它包含

    • 用户创建的所有年份列表
    • 特定年份订阅用户按月分组的列表
    • 特定年份订阅用户按性别分组的列表
    • 特定年份订阅用户按职业分组的列表
     public class DashBoardDTO
        {
            public List<int> Years { get; set; }
            public List<LineChartDataDTO> SubscribedUsersForYearGroupedByMonth { get; set; }
            public List<PieChartDataDTO> SubscribedUsersForYearGroupedByGender { get; set; }
            public List<PieChartDataDTO> 
                   SubscribedUsersForYearGroupedByProfession { get; set; }
        }
  • DashBoardWebApp.Application/UseCases/DashBoard/services 中创建 IDashboardService 接口。

     public interface IDashboardService
        {
            DashBoardDTO GetSubscribedUsersStatsByYear(int? year);
        }
  • DashBoardWebApp.Application/UseCases/DashBoard/services 中创建 DashboardService

    此类包含由合同公开的各种方法的实现。

     public class DashboardService : IDashboardService
        {
            private IUserRepository _userRepository;
            public DashboardService(IUserRepository userRepository)
            {
                this._userRepository = userRepository;
            }
    
            public DashBoardDTO GetSubscribedUsersStatsByYear(int? year)
            {
                DashBoardDTO dashBoard = new DashBoardDTO();
            
                dashBoard.Years = this._userRepository.GetAllCreatedUsersYears();
    
                if (dashBoard.Years == null || dashBoard.Years.Count == 0)
                {
                    return dashBoard;
                }
    
                if (!year.HasValue)
                {
                    //if year not exists then set it with the last year from years list.
                    year = dashBoard.Years.LastOrDefault();
                }
    
                List<User> subsribedUsers = this._userRepository.GetUsersByYear(year.Value);
               
                if (subsribedUsers?.Count == 0)
                {
                    return dashBoard;
                }
    
                dashBoard.SubscribedUsersForYearGroupedByMonth = 
                          subsribedUsers.GroupBy(g => g.CreatedAt.Month).Select
                          (g => new LineChartDataDTO(g.First().CreatedAt, g.Count())).ToList();
    
                var totalCount = subsribedUsers.Count;
                
                dashBoard.SubscribedUsersForYearGroupedByGender = subsribedUsers.GroupBy
                          (g => g.Gender).Select(g => new PieChartDataDTO(g.Key, g.Count()* 
                          100/(decimal)totalCount )).ToList(); 
                dashBoard.SubscribedUsersForYearGroupedByProfession = 
                          subsribedUsers.GroupBy(g => g.Profession.Name).Select
                          (g => new PieChartDataDTO(g.Key, g.Count() * 
                          100 / (decimal)totalCount )).ToList();
    
                dashBoard.SubscribedUsersForYearGroupedByGender.Last().Value = 
                100 - dashBoard.SubscribedUsersForYearGroupedByGender.Where(d => d != 
                dashBoard.SubscribedUsersForYearGroupedByGender.Last()).Sum(d => d.Value);
                dashBoard.SubscribedUsersForYearGroupedByProfession.Last().Value = 
                100 - dashBoard.SubscribedUsersForYearGroupedByProfession.Where
                (d => d != dashBoard.SubscribedUsersForYearGroupedByProfession.Last()).Sum
                (d => d.Value);
    
                return dashBoard;
            }
        }

    最后,我们在 DI 系统中声明 DashboardService 类,以便在每次请求时自动在我们的控制器中实例化它。我们需要将以下指令添加到 Startup 类的 ConfigureServices 中。

    services.AddScoped<IDashboardService, DashboardService>();

III) 实现 Web 服务

后端部分的最后一步是为已开发用例创建一个入口点。为此,我们应该创建一个 **Dashboard Web API** 类,该类公开我们已开发用例的端点。它将只包含一个名为 FilterByYear 的方法,该方法返回有关特定年份订阅用户的所需所有信息。

[Route("api/dashboard")]
    [ApiController]
    public class DashboardApi : ControllerBase
    {
        private readonly IDashboardService _dashboardService;

        public DashboardApi(IDashboardService dashboardService)
        {
            this._dashboardService = dashboardService;
        }

        [HttpGet("{year:int?}")]
        public IActionResult FilterByYear([FromRoute] int? year)
        {
            return Ok(this._dashboardService.GetSubscribedUsersStatsByYear(year));
        }
    }

IV) 测试 Web API

要测试我们的 Web API 服务,我们可以使用 Postman 来创建和执行 HTTP REST 请求。

  • 从 Visual Studio 启动项目。
  • 使用 Postman 创建一个新的 REST 请求。

  • 执行请求。

实现前端

  • 首先,我们需要将 chartJsmoment.js 库导入到布局页面(路径:Views/Home/_Layout.chtml)。

  • 接下来,修改主页(路径:Views/Home/Index.chtml)以显示主要过滤器、折线图和两个饼图。
    @{
        ViewData["Title"] = "Home Page";
    }
    
    <div class="text-center">
        <p><h4>Developing a dashboard web application with ASP.NET MVC Core, 
               WEB Api, JavaScript, PostegreSql and ChartJs</h4></p>
        <div class="flex-d-column">
            <select id="filterByYear">
            </select>
            <div class="fullWidth">
                <canvas id="mylineChart1"></canvas>
            </div>
            <div class="flex-d-row fullWidth">
                <div class="chart-container">
                    <canvas id="mypieChart1"></canvas>
                </div>
                <div class="chart-container">
                    <canvas id="mypieChart2"></canvas>
                </div>
            </div>
        </div>
    </div>
  • 接下来,通过创建 dashboard.css 文件(路径:wwwroot/css/dashboard.css)为首页添加一些样式,然后将其包含在布局页面中。
    .fullWidth {
        width: 100%
    }
    
    .flex-d-column {
        display: flex;
        flex-direction: column;
    }
    
    .flex-d-row {
        display: flex;
        flex-direction: row;
    }
    
    .chart-container {
        flex: 1;
    }
  • 然后,使用 JavaScript 语言为首页实现操作:我们需要创建一个 dashboard.js(路径:wwwroot/js/dashboard.js),它将由上述函数列表组成。
    • drawLineChart:此函数创建一个用于在特定画布上绘制或更新图表的饼图配置。
    • drawPieChart:此函数创建一个用于在特定画布上绘制或更新图表的饼图配置。
    • drawChart:使用上述函数创建的设置,它将更新现有的图表实例(如果存在)或在特定画布上创建一个新图形,并返回一个将用于将来更新的新实例。
    • makeRandomColor:将返回随机十六进制颜色。此函数在每次更新操作时为不同的图表分配随机颜色。
    • filterDashboardDataByYear:这是我们的主函数,在全局年份过滤器检测到任何更改后都会触发。它将向仪表板 API 发送请求并获取响应 data(),该响应将显示在专用图表中。
    $(document).ready(function () {
        let lineChart1 = null;
        let pieChart1 = null;
        let pieChart2 = null;
    
        function drawChart(chartInstance, canvasId, chartSettings) {
           
            if (chartInstance != null) {
                //update chart with new configuration
                chartInstance.options = { ...chartSettings.options };
                chartInstance.data = { ...chartSettings.data };
     
                chartInstance.update();
                return chartInstance;
            } else {
                //create new chart.
                var ctx = document.getElementById(canvasId).getContext('2d');
                return new Chart(ctx, chartSettings);
            }
        }
    
        function buildSelectFilter(years, currentYear) {
            //clear all options.
            $("#filterByYear").empty();
            var selectOptionsFilterHtml = "";
    
            if (years) {
                years.forEach((year) => {
                    selectOptionsFilterHtml += `<option value="${year}" 
                          ${currentYear == year ? 'selected':''}>${year}</option>`
                });
            }
    
            $("#filterByYear").append(selectOptionsFilterHtml);
        }
    
        function makeRandomColor() {
            return "#" + Math.floor(Math.random() * 16777215).toString(16);
        }
    
        function drawLineChart(chartInstance, canvasId, data, titleText) {
    
            let settings = {
                // The type of chart we want to create
                type: 'line',
                // The data for our dataset
                data: {
                    datasets: [{
                        backgroundColor: 'rgba(255,0,0,0)',
                        borderColor: makeRandomColor(),
                        data: data
                    }]
                },
    
                // Configuration options go here
                options: {
                    legend: {
                        display: false
                    },
                  
                    title: {
                        display: true,
                        text: titleText,
                        fontSize: 16
                    },
                    scales: {
                        xAxes: [{
                            type: 'time',
                            time: {
                                unit: 'month',
                                displayFormats: {
                                    month: 'MM YYYY'
                                }
                            }
                        }]
                    }
                }
            };
    
            return drawChart(chartInstance, canvasId, settings);
        }
    
        function drawPieChart(chartInstance, canvasId, data, labels, titleText) {
    
            //generate random color for each label.
            let bgColors = [];
    
            if (labels) {
                bgColors = labels.map(() => {
                    return makeRandomColor();
                });
            }
    
            var settings = {
                // The type of chart we want to create
                type: 'pie',
    
                // The data for our dataset
                data: {
                    labels: labels,
                    datasets: [{
                        backgroundColor: bgColors,
                        borderColor: bgColors,
                        data: data
                    }],
                },
    
                // Configuration options go here
                options: {
                    tooltips: {
                        callbacks: {
                            label: function (tooltipItem, data) {
                                //create custom display.
                                var label = data.labels[tooltipItem.index] || '';
                                var currentData = data.datasets[0].data[tooltipItem.index];
    
                                if (label) {
                                    label = `${label} ${Number(currentData)} %`;
                                }
    
                                return label;
                            }
                        }
                    },
                    title: {
                        display: true,
                        text: titleText,
                        fontSize: 16
                    },
                }
            };
    
            return drawChart(chartInstance, canvasId, settings);
        }
    
        function filterDashboardDataByYear(currentYear) {
    
            currentYear = currentYear || '';
            let url = `https://:65105/api/dashboard/${currentYear}`;
    
            $.get(url, function (data) {
    
                if (!currentYear && data.years.length > 0) {
                    //pick the last year.
                    currentYear = data.years.reverse()[0];
                }
    
                buildSelectFilter(data.years, currentYear);
                
                let data1 = [];
                if (data.subscribedUsersForYearGroupedByMonth) {
                    data1 = data.subscribedUsersForYearGroupedByMonth.map
                            (u => { return { "x": moment(u.x, "YYYY-MM-DD"), "y": u.y } });
                }
    
                let data2 = [];
                let labels2 = []; 
                if (data.subscribedUsersForYearGroupedByGender) {
                    data2 = data.subscribedUsersForYearGroupedByGender.map(u => u.value);
                    labels2 = data.subscribedUsersForYearGroupedByGender.map(u => u.label);
                }
    
                let data3 = [];
                let labels3 = [];
                if (data.subscribedUsersForYearGroupedByProfession) {
                    data3 = data.subscribedUsersForYearGroupedByProfession.map(u => u.value);
                    labels3 = 
                    data.subscribedUsersForYearGroupedByProfession.map(u => u.label);
                }
              
                lineChart1 = drawLineChart(lineChart1, "mylineChart1", data1, 
                             `Number of subscribed users per month in ${currentYear}`);
                pieChart1 = drawPieChart(pieChart1, "mypieChart1", data2, labels2, 
                            `Number of subscribed users in ${currentYear} 
                             grouped by gender`);
                pieChart2 = drawPieChart(pieChart2, "mypieChart2", data3, labels3, 
                            `Number of subscribed users in 
                             ${currentYear} grouped by profession`);
            });
        }
    
        filterDashboardDataByYear();
    
        $(document).on("change", "#filterByYear", function () {
            filterDashboardDataByYear(parseInt($(this).val()));
        });
    });     

此 JS 文件应导入到布局页面。

运行应用程序

当我们首次运行应用程序时,显示的数据将基于用户创建的最后一年进行过滤。

我们可以从 组合框 中选择不同的年份来显示与所选年份相关的其他用户统计数据。

参考文献

关注点

希望您喜欢这篇文章。感谢您浏览我的帖子,请尝试下载源代码,不要犹豫留下您的问题和评论。

历史

  • v1 2021 年 1 月 24 日:初始版本
© . All rights reserved.