使用ASP.NET Core,JavaScript,PostegreSql和ChartJs的动态仪表板Web应用程序

目录

介绍

先决条件

创建项目架构

创建数据库

实现后端

一)创建DataAccess

1)创建实体和关系

2)设置数据库

3)创建存储库

II)实现应用逻辑

III)实现Web服务

IV)测试Web API

实现前端

运行应用程序

参考


通过本文,我们将使用.NET MVC Core项目从头开始构建一个漂亮的仪表板网页。在此处概述的步骤中,您将学习干净的体系结构,实体框架代码优先方法,开发Web服务以及使用chartjs

介绍

在专业领域,公司的CEO或合格的经理希望快速访问所有关键数据点,以帮助分析、比较和做出相关决定。

仪表板是一种对公司的基本数据具有全局视角的方法,这些仪表板的一些用例是比较特定两年的净销售额、网站每年每月的订阅者数量和Azure租户的订阅者数量。 仪表板通常由图表和表格表示。

有很多JavaScript库可帮助构建可用于的漂亮图形视觉,其中最好的是ChartJs

通过本文,我们将构建一个漂亮的仪表板Web应用程序,其中显示有关已订阅用户的一些指标。该应用程序将使用C#,ASP.NET MVC CoreJavaScriptChartJs库构建。

在阅读本文之后,您将了解更多有关:

  • 干净的架构
  • 使用代码优先方法的实体框架核心
  • 依赖注入
  • PostgreSQL数据库
  • CharJs
  • 使用PostMan测试API端点

先决条件

要理解本文,您应该具有有关ASP.NET MVC CoreJavaScript的基本知识。

创建项目架构

在本教程中,我们将采用简洁的架构原理从头开始开发我们的应用程序。

采用纯净的架构,由于关注点的分离,我们的应用程序将在可维护性和可测试性方面获得很多收益,并且它不会专注于特定的框架或技术,而是专注于领域逻辑。建议您访问此链接此链接,以获取完整的定义并深入了解此类最佳实践。

我们的项目将包括四个部分:

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

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

创建数据库

由于使用了EF Framework,我们的应用程序将独立于数据库,我们可以插入任何类型的数据库,例如PostgreSqlOracleSqlServer数据库,我们只需要更改提供程序即可。

对于此应用程序,我们将使用PostgreSql,首先我们需要将其安装在本地计算机上并创建一个新的空数据库,以执行以下操作:

  • 从此链接下载并安装pgadmin4 
  • 从此链接下载并安装pgAgent 
  • 创建一个空数据库:我们需要启动pgadmin应用程序并创建一个新数据库,如下所示:

实现后端

一)创建DataAccess

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

通过使用以下技术或框架之一来确保通信:ADO.NETEFNHibernate等,它们都具有相同的主要目标,这使应用程序和数据库之间的对话过程更加容易且透明。

对于我们的应用程序,我们将使用EF(实体框架),它是.NET Core项目中使用最流行的ORM,它具有多种优点,例如:

  • 领域类和关系数据之间的映射。
  • 通过使用对实体的Linq引入更多抽象来管理和收集数据库中的数据。
  • 可以支持不同的关系数据库系统,例如PostgreSqlOracleSqlServer
  • 提供多种方法,例如代码优先、数据库优先、模型优先。
  • 借助延迟加载机制,可以按需加载数据。
  • 与诸如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 Package Manager安装EF Core软件包和EFPostegreSql提供程序:

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

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);
    }
}
  • 创建种子数据:

我们需要通过添加使用数据初始化数据库的种子方法来创建扩展ModelBuilder函数的ModelBuilderExtensions类,该类将在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查询映射对象(我们的业务实体)。

该实现将进入基础结构项目,因为它依赖于外部资源,并将使用接口和依赖项注入(DI)公开给其他项目。

  • 创建通用存储库类:

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

    • Delete(TEntity entity):从数据库上下文中删除实体,此操作将在调用context.savechanges()后应用于数据库。
    • GetAll(Expression<Func<TEntity, bool>> filter = null, List<string> propertiesToInclude = null):返回与filter参数传递的条件匹配的所有实体。由于实体框架及时加载,返回的结果可以包含'propertiesToInclude'参数指定的所有指定关系。
    • 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();
        }
    }

一旦完成了存储库的实现,就可以使用它们来构建我们的应用程序逻辑吗?但是,在此之前,我们需要通过修改Startup class以下ConfigureServices方法将其声明为依赖注入(DI)系统

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模型:

它保存折线图中的点的xy坐标。

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 = `http://localhost: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文件应导入到布局页面中。

运行应用程序

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

我们可以通过combobox选择其他年份,以显示与所选年份相关的其他用户统计信息。

参考

https://www.codeproject.com/Articles/5292975/Dynamic-Dashboard-Web-Application-using-ASP-NET-Co

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值