将具有不同关系值的 table 转换为 excel 列

Convert a table with different relational values to excel columns

我有这些表:

类别

CategoryId
CategoryTitle
...
ICollection<Article> Articles  

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

文章

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

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

文章评论

ArticleReviewId 
ReviewPoint
ArticleId
ReviewerId

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

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; }
}

注意:由于一篇文章的评论数不同,所以我使用一对多关系,但是在final结果所有这些都应该排成一行。现在我创建不属于数据库的新 class 并将此 class 传递给 ExcelPackage class 以生成 xlsx 作为输出:

ExcelExport

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

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


编辑
这是我预期的 excel 输出

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

假设您有以下模型:

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();
}

How can I populate the excelExport class used another 3 classes?

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

NumberOfComment 等于 article.NumberOfComment 每条条目! 除非你雇用另一个名为ArticleComment的table并利用导航属性Article class(使用public virtual ICollection<ArticleComment> Comments { get; set;}),然后用article.Comments.Count()统计评论数。

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

每篇文章的

Reviews 可以是如下内容:

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

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

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

根据 OP 的编辑进行编辑

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

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

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

希望这就是您要找的。据我所知,目标是“扁平化”来自给定“classes”的数据。我将放弃导出到 Excel,因为这似乎是一个不同的问题。 “类” 存在,我猜一个返回 DataTable 或任何你想要的“collection” 类型的方法。我猜这会使导出到 Excel.

时变得更容易

Catergoryclass中有一个Article’s的“collection”。每个 Article 代表 collection(Excel 电子表格)中的一个“行”。每个 Article 都有一个 ArticleReviews 的“collection”,称为 Reviews。正如你所说...

One of my problems is Reviewer Point column is dynamically changed, for one article may be there 3 column (like upper image) but in another may be 4 or 5 Reviewer Point.

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

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

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

其中一个可能的问题是列顺序可能是不确定的table。根据哪篇文章排在第一位,将确定列的顺序。因此,我建议对列进行一些“排序”以保持一定的顺序。

我添加了一个 class Reviewer 应该有助于排序和比较列名。它是一个简单的 Reviewer class ,如下所示。请注意排序使用的 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 class,因此需要进行一些更改。一些变量似乎是不必要的,并且只显示需要的变量。主要变化是上面的 Reviewer object 来定义审稿人。

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;
  }
}

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

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 class 包含所有 Article。这个 class 是我们需要做前面描述的所有专栏内容的地方。第一部分是获得没有重复的 List<Reviewer>。这将需要遍历所有文章,然后遍历每篇文章中的所有评论。在此过程中,我们可以检查“评论者”并创建一个包含所有用户的 non-duplicated 列表。代码创建一个新的空 List<Reviewer> 然后循环遍历每篇文章,循环遍历每篇评论。检查“审稿人”是否已经在列表中,如果不在,则添加它们,否则忽略重复的“审稿人”。对列表进行排序以保持列顺序,然后将其返回。

我猜这个列表可以用多种方式来解决“列”难题。在这个例子中,另一个方法被添加到 Category class。 GetDataTable方法returns一个DataTable来自文章中的数据。首先将前四列添加到 table、“Title”、“#ofView”、“#ofComment”和“Average point”。接下来循环遍历所有审阅者以添加审阅者列。审阅者姓名用作列名称。这就是我们在添加数据时识别哪一列属于哪个审阅者的方式。

最后,循环遍历每个 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"));
  }
}

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)。

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

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<Article> 转换为 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},
        },
    },
};