目录

介绍

先决条件

创建项目架构

创建数据库

实现后端

一)创建DataAccess

1)创建实体和关系

2)设置数据库

3)创建存储库

II)实现应用逻辑

III)实现Web服务

IV)测试Web API

实现前端

运行应用程序

参考


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

  • 下载源代码(RAR)-854.9 KB

介绍

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

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

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

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

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

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

先决条件

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

创建项目架构

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

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

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

  • UI:组成如下:

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

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

创建数据库

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

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

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

实现后端

一)创建DataAccess

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

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

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

  • 领域类和关系数据之间的映射。
  • 通过使用对实体的Linq引入更多抽象来管理和收集数据库中的数据。
  • 可以支持不同的关系数据库系统,例如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 Package Manager安装EF Core软件包和EF的PostegreSql提供程序:

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

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 Requiredprotected 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或更复杂的数据源操作变得不同的方法。

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

该实现将进入基础结构项目,因为它依赖于外部资源,并将使用接口和依赖项注入(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模型:

它保存折线图中的点的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请求。

  • 执行请求。

实现前端

  • 首先,我们需要导入chartJs和moment.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 configurationchartInstance.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 createtype: 'line',// The data for our datasetdata: {datasets: [{backgroundColor: 'rgba(255,0,0,0)',borderColor: makeRandomColor(),data: data}]},// Configuration options go hereoptions: {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 createtype: 'pie',// The data for our datasetdata: {labels: labels,datasets: [{backgroundColor: bgColors,borderColor: bgColors,data: data}],},// Configuration options go hereoptions: {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选择其他年份,以显示与所选年份相关的其他用户统计信息。

参考

  • 干净的架构
  • ChartJs
  • PostegreSql
  • 实体框架
  • Linq to Entities
  • Dotnet迁移工具

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

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

  1. netcore 编译 html dll,ASP.NET Core Razor 视图预编译、动态编译

    0x01 前言 ASP.NET Core在默认发布情况下,会启动预编译将试图编译成xx.Views.dll,也许在视图中打算修改一处很细小的地方我们需要再重新编译视图进行发布.下面我将从 ASP.NE ...

  2. [ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?

    昨天有个朋友在微信上问我一个问题:他希望通过动态脚本的形式实现对ASP.NET Core MVC应用的扩展,比如在程序运行过程中上传一段C#脚本将其中定义的Controller类型注册到应用中,问我是 ...

  3. ASP.NET Core Razor 视图预编译、动态编译

    0x01 前言 ASP.NET Core在默认发布情况下,会启动预编译将试图编译成xx.Views.dll,也许在视图中打算修改一处很细小的地方我们需要再重新编译视图进行发布.下面我将从 ASP.NE ...

  4. ASP.NET Core依赖注入容器中的动态服务注册

    介绍 在ASP.NET Core中,每当我们将服务作为依赖项注入时,都必须将此服务注册到ASP.NET Core依赖项注入容器.但是,一个接一个地注册服务不仅繁琐且耗时,而且容易出错.因此,在这里,我 ...

  5. 使用依赖注入的ASP.NET Core 2.0用户角色基础动态菜单管理

    目录 介绍 背景 先决条件 使用代码 第1步 - 创建数据库 创建数据库 第2步 - 创建ASP.NET Core 更新appsettings.json 步骤3 - 在Startup.cs文件中添加I ...

  6. JavaScript 使用面向对象的技术创建高级 Web 应用程序

    最近,我面试了一个有五年 Web 应用程序开发经验的软件开发人员.四年半来她一直在从事 JavaScript 相关的工作,她自认为 JavaScript 技能非常好,但在不久之后我就发现实际上她对 J ...

  7. chartjs 动态数据_使用ChartJS构建动态仪表板

    chartjs 动态数据 今天,我们将通过ChartJS的功能使用实时图表创建一个动态仪表板. ChartJS是一个功能强大的,无依赖项JavaScript库,可通过canvas元素构建图形. 最重要 ...

  8. 【ASP.net】--WebSite(网站) 和 WebApplication(Web应用程序)

    前言 :           在利用VS2010创建web项目的时候,会有两个选择.可以选择直接创建website网站,也可以Webapplication应用程序.刚刚接触web开发,看到这两个就疑惑 ...

  9. Asp.NET Core 如何使用ElasticSearch和Kibana创建仪表板

    图片 在我以前的文章(这里是第一[1]篇和第二篇[2])中,我展示了ElasticSearch作为电子商务中的全文搜索引擎的使用,一些高级配置的设置和使用以及products包含所有内容的索引的创建保 ...

最新文章

  1. openstack M 版 neutron网络组件基础入门
  2. golang中的strings.SplitN
  3. 微信公众平台开发书籍推荐
  4. luoguP1082同余方程
  5. 盘点三个JavaScript案例——实现限时秒杀、定时跳转、改变盒子大小
  6. 学游戏3D建模,选机构要注意哪些方面?
  7. UI素材模板|优秀的后台管理系统UI面板
  8. ElasticSearch预警服务-Watcher详解-Schedule配置
  9. java静态成员静态代码块初始化顺序
  10. Java基础-Java语言简介
  11. php获取笔顺矢量,PHP获取汉字笔画数功能【测试可用】
  12. k近邻法 kd树 平衡kd树
  13. 安装pip的三种方法
  14. java小游戏_Java开发小游戏,30分钟教会你
  15. 提高计算机启动速度的是什么,三种方法让你实现电脑秒开!提升电脑开机速度就是这么简单!...
  16. 织梦后台怎么上传mp4视频到网站中
  17. 【gev】 Golang 实现轻量、快速的基于 Reactor 模式的非阻塞 TCP 网络库
  18. 第十七届“振兴杯”全国青年 职业技能大赛——计算机程序设计员(云计算平台与运维)参赛回顾与总结
  19. Neutron服务组件
  20. 【LintCode】Backpack 背包问题

热门文章

  1. linux 笔记服务器,Linux服务器 CentOS7.5 操作小笔记
  2. python读hadoop_python读取hdfs并返回dataframe教程
  3. python爬视频网站数据_python爬虫基础应用----爬取无反爬视频网站
  4. linux网速卡怎么办_手机同是4G网络,为什么总比别人慢,学会这个方法,网速成倍提升...
  5. mysql 5.6.32 linux_centos7 下手动安装MySQL-5.6.32-1.linux_glibc2.5.x86_64.rpm-bundle
  6. mongodb objetcid_mongodb(1)
  7. 设计灵感|如何做好网页后台数据展示的设计?
  8. 平面设计模板素材|越发流行的霓虹渐变趋势
  9. C4D模型库!你想要的模型这里都有
  10. mysql中密码存入加密,如何在数据库中存储加密的密码?