【问题标题】:Convert a table with different relational values to excel columns将具有不同关系值的表转换为excel列
【发布时间】:2019-05-10 13:32:38
【问题描述】:

我有这些表:

类别

CategoryId
CategoryTitle
...
ICollection<Article> Articles  

每个分类可以有几篇文章:

文章

ArticleId
ArticleTitle  
NumberOfComment
NumberOfView
...
ICollection<ArticleReview> Reviews 

每篇文章都有一些用户的评论:

文章评论

ArticleReviewId 
ReviewPoint
ArticleId
ReviewerId

我正在尝试使用 EPPlus 包导出 excel 报告
这是我的ExcelExport 课程:

public class excelExport 
{
    public string ArticleTitle { get; set; }

    public int NumberOfComment { get; set; }
    public int NumberOfReviews { get; set; }
    public List<ResearchReviewReport> Reviews { get; set; }
}

public class ArticleReviewReport
{
    public string Reviewer { get; set; }
    public int ReviewPoint { get; set; }
}

注意:由于一篇文章的评论数量不同,我使用一对多的关系,但最终的结果应该是所有的都被展平单行。现在我创建了不属于数据库的新类,并将这个类传递给ExcelPackage 类以生成 xlsx 作为输出:

Excel导出

ArticleTitle 
Reviewer1Point
Reviewer2Point
............
ReviewerNPoint
ReviewersAvaragePoint
NumberOfComment
NumberOfView  

如何使用另外 3 个类填充 ExcelExport 类?


编辑
这是我预期的 excel 输出

我的一个问题是 Reviewer Point 列是动态更改的,
一篇文章可能有 3 列(如上图),但另一篇文章可能有 4 或 5 个评论者点。
Edit2
我忘了说每篇文章都有一些问题和每个问题的审稿人回答,所以如果我们有 3 个问题并且有 2 个审稿人,文章有 6 个 ArticleReview,我应该得到每个审稿人的平均 ArticleReview 并将其放在单个单元格中

【问题讨论】:

    标签: c# .net asp.net-mvc entity-framework code-first


    【解决方案1】:

    为简单起见,我假设您正在使用以下简化模型并将描述解决方案。您可以轻松地使其适应您的模型:

    public class Article
    {
        public string Title { get; set; }
        public DateTime Date { get; set; }
        public List<Review> Reviews { get; set; }
    }
    public class Review
    {
        public int Points { get; set; }
    }
    

    现在我们将根据输入数据生成以下输出,具有动态数量的审阅者列:

    解决方案

    创建一个将List&lt;Article&gt; 转换为DataTable 的函数就足够了。要创建这样的DataTable,为Article 的每个属性添加一个新列。然后找到Reviews 列表的最大计数并添加该列数。然后在一个循环中,对于每个Article,包括它的Review 列表,创建一个对象数组并添加到DataTable。显然你也可以对字段进行计算。

    函数如下:

    public DataTable GetData(List<Article> list)
    {
        var dt = new DataTable();
        dt.Columns.Add("Title", typeof(string));
        dt.Columns.Add("Date", typeof(DateTime));
        var max = list.Max(x => x.Reviews.Count());
        for (int i = 0; i < max; i++)
            dt.Columns.Add($"Reviewer {i + 1} Points", typeof(int));
        foreach (var item in list)
            dt.Rows.Add(new object[] { item.Title, item.Date }.Concat(
                item.Reviews.Select(x => x.Points).Cast<object>()).ToArray());
        return dt;
    }
    

    测试数据

    这是我的测试数据:

    var list = new List<Article>
    {
        new Article(){
            Title = "Article 1", Date = new DateTime(2018,1,1),
            Reviews = new List<Review> {
                new Review(){Points=10},
            },
        },
        new Article(){
            Title = "Article 2", Date = new DateTime(2018,1,2),
            Reviews = new List<Review> {
                new Review(){Points=10}, new Review(){Points=9}, new Review(){Points=8},
            },
        },
        new Article(){
            Title = "Article 3", Date = new DateTime(2018,1,3),
            Reviews = new List<Review> {
                new Review(){Points=9},
            },
        },
    };
    

    【讨论】:

    • 更新了使用Concat连接数组的方法。
    • 为什么不使用 TypeDescriptor.GetProperties 添加列?
    • 可以使用。这只是一个示例作为起点。只要保留我的,如果您使用TypeDescriptor.GetProperties,您需要排除列表属性。
    • 我用这篇文章导出excel,c-sharpcorner.com/article/export-to-excel-in-asp-net-mvc这个单独创建列标题并传递给ExportExcel方法
    • 我明白了。现在你有了DataTable(解决了最大的问题,处理列表中可变数量的项目并得到一个平坦的结果。)下一步(这是最简单的部分)是将DataTable导出到excel。
    【解决方案2】:

    假设您有以下模型:

    public class Category
    {
        public long CategoryId { get; set; }
        public string CategoryTitle { get; set; }
        public virtual ICollection<Article> Articles { get; set; }
    }
    
    public class Article
    {
        public long ArticleId { get; set; }
        public long CategoryId { get; set; }
        public string ArticleTitle { get; set; }
        public int NumberOfComment { get; set; }
        public int NumberOfView { get; set; }
        public virtual Category Category { get; set; }
        public virtual ICollection<ArticleReview> Reviews { get; set; }
    }
    public class ArticleReview
    {
        public long ArticleReviewId { get; set; }
        public long ArticleId { get; set; }
        public string ReviewerId { get; set; }
        public int ReviewPoint { get; set; }
        public virtual Article Article { get; set; }
    }
    public class ExcelExport
    {
        public string ArticleTitle { get; set; }
        public int NumberOfComment { get; set; }
        public int NumberOfReviews { get; set; }
        public List<ArticleReviewReport> Reviews { get; set; }
    }
    
    public class ArticleReviewReport
    {
        public string Reviewer { get; set; }
        public int ReviewPoint { get; set; }
    }
    

    最终您将获得ExcelExport 的列表,查询应如下所示(_context 是您的实体 DbContext 的一个实例):

    public List<ExcelExport> GetExcelExports()
    {
        return _context.Articles.Select(a => new ExcelExport
        {
            ArticleTitle = a.ArticleTitle,
            NumberOfComment = a.NumberOfComment,
            NumberOfReviews = a.NumberOfView,
            Reviews = a.Reviews.Select(r => new ArticleReviewReport
            {
                Reviewer = r.ReviewerId,
                ReviewPoint = r.ReviewPoint
            }).ToList()
        }).ToList();
    }
    

    【讨论】:

    • ArticleReviewReport 如何在单行中为每篇文章扁平化?每个表格也是独立的
    • 这完全破旧了,我想在单列上显示每条评论,但它会返回它的列表!
    【解决方案3】:

    我希望这是您正在寻找的。据我所知,目标是“扁平化”给定“类”中的数据。我将放弃导出到 Excel,因为这似乎是一个不同的问题。 “类”存在,我猜是返回 DataTable 或您想要的任何“集合”类型的方法。我猜这会使导出到 Excel 时变得更容易。

    Catergory 类中,它有一个Article’s 的“集合”。每个Article 代表集合(Excel 电子表格)中的一个“行”。每个Article 都有一个ArticleReviews 的“集合”,称为Reviews。正如你所说……

    我的一个问题是 Reviewer Point 列是动态更改的, 一篇文章可能有 3 列(如上图),但在 另一个可能是 4 或 5 个 Reviewer Point。

    听起来每个Article 可能有很多审稿人,此外,并非所有审稿人都会“审阅”所有文章。鉴于这一点以及“扁平化”这些数据的要求,这意味着为“每个”审阅者创建一个列。另外,我假设只列出了审过一篇文章的审稿人,否则,为每个审稿人创建一个列会很简单。我猜目标是只有评论者“评论”了至少一 (1) 篇文章的专栏。

    话虽如此,我猜第一个问题是弄清楚我们需要为审稿人提供“多少”列,以及这些审稿人的姓名是“什么”。我们需要一些方法来识别“哪个”列属于哪个审阅者。我使用Reviewer 名称来识别正确的列。那么我们如何找到审稿人……

    Category 类有一个Artlicles 列表很方便。如果创建了一个方法来遍历每篇文章,然后遍历文章的每条评论并收集所有评论者并忽略重复项……这应该为我们提供需要为其添加列的“评论者”列表。如果该方法重新调整了Reviewer 的列表,我们可以使用它来确定我们需要多少列,以及这些列的名称应该是什么。

    其中一个可能的问题是列顺序可能无法预测。根据首先是哪篇文章,将确定列的顺序。因此,我建议对列进行一些“排序”以保持某种顺序。

    我添加了一个类Reviewer,它应该有助于对列名进行排序和比较。这是一个简单的Reviewer 类,如下所示。请注意排序使用的compareTo 方法。它按审阅者 ID 排序。这将保持相同的列顺序。

    public class Reviewer : IComparable<Reviewer> {
    
      public int ReviewerID { get; set; }
      public string ReviewerName { get; set; }
    
      public Reviewer() {
      }
    
      public Reviewer(int reviewerID, string reviewerName) {
        ReviewerID = reviewerID;
        ReviewerName = reviewerName;
      }
    
      public override string ToString() {
        return "ReviewerID: " + ReviewerID.ToString();
      }
    
      public override bool Equals(object obj) {
        return this.ReviewerName.Equals(((Reviewer)obj).ReviewerName);
      }
    
      public override int GetHashCode() {
        return ReviewerName.GetHashCode();
      }
    
      public int CompareTo(Reviewer other) {
        return this.ReviewerID.CompareTo(other.ReviewerID);
      }
    }
    

    这将影响ArticleReview 类,需要在那里进行一些更改。有些变量看起来是不必要的,只显示需要的变量。主要变化是上面的Reviewer 对象来定义审阅者。

    public class ArticleReview {
    
      public long ArticleId { get; set; }
      public Reviewer TheReviewer { get; set; }
      public int ReviewPoint { get; set; }
    
      public ArticleReview() {
      }
    
      public ArticleReview (long articleId, Reviewer reviewerId, int reviewPoint) {
        ArticleId = articleId;
        TheReviewer = reviewerId;
        ReviewPoint = reviewPoint;
      }
    }
    

    接下来是Article 类。它包含该文章的所有评论。似乎有一个名为“平均点”的列。这看起来像是评论中的“计算”值。因此,我猜想Article 类可以方便地为我们“计算”这个值。它包含所有评论……所需要的只是将所有分数相加并除以评论数量。这个方法被添加到Article 类中。

    public class Article {
      public long ArticleId { get; set; }
      public string ArticleTitle { get; set; }
      public int NumberOfComment { get; set; }
      public int NumberOfView { get; set; }
      public virtual ICollection<ArticleReview> Reviews { get; set; }
    
      public Article() {
      }
    
      public Article(long articleId, string articleTitle, int numberOfComment, int numberOfView, ICollection<ArticleReview> reviews) {
        ArticleId = articleId;
        ArticleTitle = articleTitle;
        NumberOfComment = numberOfComment;
        NumberOfView = numberOfView;
        Reviews = reviews;
      }
    
      public decimal GetAverage() {
        if (Reviews.Count <= 0)
          return 0;
        decimal divisor = Reviews.Count;
        int totPoints = 0;
        foreach (ArticleReview review in Reviews) {
          totPoints += review.ReviewPoint;
        }
        return totPoints / divisor;
      }
    }
    

    最后,Category 类包含所有 Articles。这个类是我们需要完成前面描述的所有列内容的地方。第一部分是得到一个没有重复的List&lt;Reviewer&gt;。这将需要遍历所有文章,然后遍历每篇文章中的所有评论。在此过程中,我们可以检查“审阅者”并创建所有用户的非重复列表。代码创建一个新的空List&lt;Reviewer&gt; 然后循环遍历每篇文章,循环遍历每条评论。检查“reviewer”是否已经在列表中,如果没有,则添加它们,否则忽略重复的“reviewer”。对列表进行排序以保持列顺序,然后将其返回。

    我猜这个列表可以通过多种方式来解决“列”难题。在这个例子中,另一个方法被添加到Category 类。 GetDataTable 方法从文章中的数据返回一个DataTable。开始前四列添加到表中,“标题”、“#ofView”、“#ofComment”和“平均点”。接下来循环遍历所有审阅者以添加审阅者列。审阅者姓名用作列名。这就是我们在添加数据时识别哪个列属于哪个审阅者的方式。

    最后,循环遍历每个Article 以添加数据。每篇文章都会创建一个新行。可以设置行中的前三列……标题、视图、评论和平均值。接下来,我们遍历所有评论。对于每条评论,targetName 设置为评论者的姓名,然后循环遍历每一列,直到找到与评论者姓名匹配的列名。找到后,我们知道这是数据所属的列。添加值并跳出列循环并进行下一次审查。

    public class Category {
      public long CategoryId { get; set; }
      public string CategoryTitle { get; set; }
      //...
      public virtual ICollection<Article> Articles { get; set; }
    
      public Category() {
      }
    
      public Category(long categoryId, string categoryTitle, ICollection<Article> articles) {
        CategoryId = categoryId;
        CategoryTitle = categoryTitle;
        Articles = articles;
      }
    
      public DataTable GetDataTable() {
        List<Reviewer> allReviewers = GetNumberOfReviewers();
        DataTable dt = new DataTable();
        dt.Columns.Add("Title", typeof(string));
        dt.Columns.Add("#ofView", typeof(long));
        dt.Columns.Add("#ofComment", typeof(long));
        dt.Columns.Add("Average point", typeof(decimal));
        foreach (Reviewer reviewer in allReviewers) {
          dt.Columns.Add(reviewer.ReviewerName, typeof(long));
        }
        foreach (Article article in Articles) {
          DataRow newRow = dt.NewRow();
          newRow["Title"] = article.ArticleTitle;
          newRow["#ofView"] = article.NumberOfView;
          newRow["#ofComment"] = article.NumberOfComment;
          newRow["Average point"] = article.GetAverage();
          foreach (ArticleReview review in article.Reviews) {
            string targetName = review.TheReviewer.ReviewerName;
            for (int i = 4; i < dt.Columns.Count; i++) {
              if (targetName == dt.Columns[i].ColumnName) {
                newRow[review.TheReviewer.ReviewerName] = review.ReviewPoint;
                break;
              }
            }
          }
          dt.Rows.Add(newRow);
        }
        return dt;
      }
    
      private List<Reviewer> GetNumberOfReviewers() {
        // we need a list of all the different reviewers
        List<Reviewer> reviewers = new List<Reviewer>();
        foreach (Article article in Articles) {
          foreach (ArticleReview review in article.Reviews) {
            if (!reviewers.Contains(review.TheReviewer)) {
              reviewers.Add(review.TheReviewer);
            }
          }
        }
        reviewers.Sort();
        return reviewers; 
      }
    }
    

    将所有这些放在一起,下面的代码会创建一些数据来演示。然后,DataTable 用作DataSourceDataGridView。我希望这会有所帮助。

    DataTable dt;
    
    public Form1() {
      InitializeComponent();
    }
    
    private void Form1_Load(object sender, EventArgs e) {
      Category cat = new Category();
      cat.CategoryId = 1;
      cat.CategoryTitle = "Category 1";
      cat.Articles = GetArticles();
      dt = cat.GetDataTable();
      dataGridView1.DataSource = dt;
    }
    
    private List<Article> GetArticles() {
      List<Article> articles = new List<Article>();
      Article art = new Article(1, "Article 1 Title", 10, 1200, GetReviews(1));
      articles.Add(art);
      art = new Article(2, "Article 2 Title", 32, 578, GetReviews(2));
      articles.Add(art);
      art = new Article(3, "Article 3 Title", 15, 132, GetReviews(3));
      articles.Add(art);
      art = new Article(4, "Article 4 Title", 13, 133, GetReviews(4));
      articles.Add(art);
      art = new Article(5, "Article 5 Title", 55, 555, GetReviews(5));
      articles.Add(art);
      art = new Article(6, "Article 6 Title", 0, 0, GetReviews(6));
      articles.Add(art);
      return articles;
    }
    
    private ICollection<ArticleReview> GetReviews(int reviewId) {
      ICollection<ArticleReview> reviews = new List<ArticleReview>();
      ArticleReview ar;
      Reviewer Reviewer1 = new Reviewer(1, "Reviewer 1");
      Reviewer Reviewer2 = new Reviewer(2, "Reviewer 2");
      Reviewer Reviewer3 = new Reviewer(3, "Reviewer 3");
      Reviewer Reviewer4 = new Reviewer(4, "Reviewer 4");
      Reviewer Reviewer5 = new Reviewer(5, "Reviewer 5");
      Reviewer Reviewer6 = new Reviewer(6, "Reviewer 6");
    
      switch (reviewId) {
        case 1:
          ar = new ArticleReview(1, Reviewer1, 15);
          reviews.Add(ar);
          ar = new ArticleReview(1, Reviewer2, 35);
          reviews.Add(ar);
          ar = new ArticleReview(1, Reviewer3, 80);
          reviews.Add(ar);
          ar = new ArticleReview(1, Reviewer5, 55);
          reviews.Add(ar);
          ar = new ArticleReview(1, Reviewer6, 666);
          reviews.Add(ar);
          break;
        case 2:
          ar = new ArticleReview(2, Reviewer1, 50);
          reviews.Add(ar);
          ar = new ArticleReview(2, Reviewer2, 60);
          reviews.Add(ar);
          ar = new ArticleReview(2, Reviewer3, 40);
          reviews.Add(ar);
          break;
        case 3:
          ar = new ArticleReview(3, Reviewer1, 60);
          reviews.Add(ar);
          ar = new ArticleReview(3, Reviewer2, 60);
          reviews.Add(ar);
          ar = new ArticleReview(3, Reviewer3, 80);
          reviews.Add(ar);
          break;
        case 4:
          ar = new ArticleReview(4, Reviewer1, 30);
          reviews.Add(ar);
          ar = new ArticleReview(4, Reviewer2, 70);
          reviews.Add(ar);
          ar = new ArticleReview(4, Reviewer3, 70);
          reviews.Add(ar);
          break;
        case 5:
          ar = new ArticleReview(5, Reviewer3, 44);
          reviews.Add(ar);
          break;
        case 6:
          break;
        default:
          break;
      }
      return reviews;
    }
    

    使用 EPPlus,下面是使用上面的 DataTable 并将 DataTable 导出到 Excel 工作表的一种方法。

    private void btn_ExportToExcel_Click(object sender, EventArgs e) {
      using (var p = new ExcelPackage()) {
        var ws = p.Workbook.Worksheets.Add("MySheet");
        ws.Cells["A1"].LoadFromDataTable(dt, true);
        p.SaveAs(new FileInfo(@"D:\Test\ExcelFiles\EpplusExport.xlsx"));
      }
    }
    

    【讨论】:

      【解决方案4】:

      如何使用另外 3 个类填充 excelExport 类?

      根据所描述的关系,您可以枚举ExcelExport 类中的每个属性,如下所示:

      NumberOfComment 等于 article.NumberOfComment 对于每个文章条目! 除非您使用另一个名为ArticleComment 的表并利用Article 类中的导航属性(使用public virtual ICollection&lt;ArticleComment&gt; Comments { get; set;}),然后用@987654327 计算cmets 的数量@。

      NumberOfReviews 等于每个文章条目的article.Reviews.Count()

      每篇文章的Reviews 可以是以下内容:

      article.Reviews.Select(s => new ArticleReviewReport { 
             Reviewer = r.ReviewerId, // user id
             ReviewPoint = r.ReviewPoint
      });
      

      看来您还必须在 ExcelExport 类中添加另一个属性以显示 ReviewersAvaragePoint 并像这样枚举:

      var reviewPoints = article.Reviews.Select(s => s.ReviewPoint);
      ReviewersAvaragePoint = reviewPoints.Sum()/reviewPoints.Count();
      

      根据 OP 的编辑进行编辑

      通过使用ArticleReviewReport 中的List(例如List&lt;ArticleReviewReport&gt; Reviews),您可以拥有一个灵活的数组(动态列)以相应的格式呈现。 缺少的部分是根据从 ArticleReview 表中提取的 Distinct ReviewerId 创建动态列。整篇文章如下所示:

      var allReviewers = db.articleReviews/*condition*/.Select(s => s.ReviewerId).Distinct();
      

      现在您可以将每个ArticleReviewReport 分配给相应的列。对于Reviews 成员,使用List&lt;Dictionary&lt;string, string&gt;&gt; 将是一个很好的数据类型

      【讨论】:

        【解决方案5】:
        public IEnumarable<ExcelExport> GetExcelExports()
        {
            return _context.Articles.Select(a => new ExcelExport
            {
               ArticleTitle = a.ArticleTitle,
               NumberOfComment = a.NumberOfComment,
               NumberOfReviews = a.NumberOfView,
               Reviewer1Point = a.Reviews.Any(e => e.ReviewerId = 1) ? a.Reviews.Where(e => e.ReviewerId = 1).Sum(e => e.ReviewPoint) : 0,
               Reviewer2Point = a.Reviews.Any(e => e.ReviewerId = 2) ? a.Reviews.Where(e => e.ReviewerId = 2).Sum(e => e.ReviewPoint) : 0,
               ....
               ReviewerNPoint = a.Reviews.Any(e => e.ReviewerId = N) ? a.Reviews.Where(e => e.ReviewerId = N).Sum(e => e.ReviewPoint) : 0
             });
        }
        

        如果您使用延迟加载,您还必须 .Include(e => e.Reviews)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-01-09
          • 2019-11-18
          • 2022-10-07
          • 2013-04-07
          • 2023-03-08
          • 1970-01-01
          • 1970-01-01
          • 2019-09-26
          相关资源
          最近更新 更多