Convert a table with different relational values to excel columns

你离开我真会死。 提交于 2021-02-07 09:47:56

问题


I have these tables:

Category

CategoryId
CategoryTitle
...
ICollection<Article> Articles  

Each category can have several articles:

Article

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

And each article has several reviews by some user:

ArticleReview

ArticleReviewId 
ReviewPoint
ArticleId
ReviewerId

i am trying to export excel report using EPPlus package
Here is my 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; }
}

Note: Since the number of reviews for an article is different, I use one-to-many relationship, but in final result all of them should be flatten in single row. Now I create new class that does not belong to database and I pass this class to ExcelPackage class to generate xlsx as output :

ExcelExport

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

How can I populate the ExcelExport class by using another 3 classes?


Edit
here is my expected excel output

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.
Edit2
i forgot to say there is some question for each article and Reviewer answer to each question ,so if we have 3 question and there is 2 Reviewer,article hase 6 ArticleReview and i should get average of ArticleReview for each Reviewer and put it in single cell


回答1:


To keep things simple, I assume you are using the following simplified models and will describe the solution. You can easily adapt it to your models:

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

Now we are going to generate the following output, having dynamic number of reviewer columns, depending to the input data:

Solution

It's enough to create a function to convert a List<Article> to a DataTable. To create such DataTable, for each property of the Article, add a new column. Then find the maximum count of Reviews list and add that number of columns. Then in a loop, for each Article including its Review list, create an array of objects and add to the DataTable. Obviously you also can perform calculation on fields.

Here is the function:

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

Test Data

Here is my test data:

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



回答2:


Assuming that you have following model:

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

Eventually you are going to have List of ExcelExport and the query should look like (_context is an instance of your Entity 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();
}



回答3:


I hope this is what you are looking for. From what I can decipher the goal is to “flatten” the data from the given “classes”. I am going to forgo the export to Excel as this appears to be a different problem. The “Classes” exist, I am guessing a method that returned a DataTable or any “collection” type you desire. I am guessing this would make things easier when exporting to Excel.

In the Catergory class, it has a “collection” of Article’s. Each Article represents a “row” in the collection (Excel spreadsheet). Each Article has a “collection” of ArticleReviews called Reviews. As you stated…

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.

This sounds like there could be many reviewers for each Article in addition, not all reviewers will necessarily “review” all articles. Given this and the requirement to “flatten” this data would mean creating a column for “each” reviewer. In addition, I assume that only reviewers who reviewed one of the articles are listed, otherwise, it would be simple to create a column for each reviewer. I am guessing the goal is to only have columns where the reviewer “reviewed” at least one (1) article.

With that said, I am guessing the first problem is figuring out “how many” columns do we need for the reviewers and “what” are those reviewers’ names are. We will need some way to identify “which” column belongs to which reviewer. I am using the Reviewer name to identify the correct column. So how do we find the reviewers…

It is convenient, that the Category class has a list of Artlicles. If a method was created that went through each article and then go through each review of the article and gather all the reviewers and ignore duplicates… this should give us the list of “reviewers” we need to add columns for. If the method retuned a list of Reviewer we could use this to determine not only how many columns we need but also what the names of those columns should be.

One possible issue in this is that the column order may be unpredictable. Depending on which article is first, is going to determine the order of the columns. Therefore, I recommend some “sorting” of the columns to maintain some order.

I added a class Reviewer that should help in the sorting and comparing column names. It is a simple Reviewer class like below. Note the compareTo method that is used by the sort. It sorts by the reviewers ID. This will keep the same column order.

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

This is going to affect the ArticleReview class and some changes are needed there. Some variables appear unnecessary, and only the needed variables are shown. The main change is the Reviewer object from above to define the 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;
  }
}

Next is the Article class. It holds all the reviews for that article. It appears there is a column called “Average point”. This looks like a “computed” value from the reviews. Therefore, I am guessing it would be convenient for the Article class to “compute” this value for us. It has all the reviews… all that is needed is to add up all the points and divide by the number of reviews. This method is added to the 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;
  }
}

Lastly the Category class holds all the Articles. This class is where we need to do all the column stuff described earlier. The first part is getting a List<Reviewer> without duplicates. This will require looping through all the articles and then looping through all the reviews in each article. In this process we can examine the “reviewers” and create a non-duplicated list of all the users. The code creates a new empty List<Reviewer> then loops through each article, the loops through each review. A check is made to see if the “reviewer” is already in the list, if not, then add them, otherwise ignore the duplicate “reviewer.” The list is sorted to maintain column order then it is returned.

I am guessing this list could be used in many ways to solve the “columns” conundrum. In this example, another method is added to the Category class. The GetDataTable method returns a DataTable from the data in the articles. To start the first four columns are added to the table, “Title” “#ofView”, “#ofComment” and “Average point.” Next a loop through all the reviewers to add the reviewer columns. The reviewer name is used as the column name. This is how we identify which column belongs to which reviewer when adding the data.

Finally, a loop through each Article to add the data. Each article creates a new row. The first three columns in the row can be set… Title, view, comment and Average. Next, we loop through all the reviews. For each review targetName is set to the reviewer’s name, then a loop through each column until it finds the column name that matches the reviewers name. When found we know that this is the column the data belongs in. Add the value and break out of the columns loop and get the next review.

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

Putting this all together, the code below creates some data to demonstrate. Then, the DataTable is used as a DataSource to a DataGridView. I hope this helps.

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

Using EPPlus, below is one way to use the DataTable above and export the DataTable to an Excel worksheet.

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:


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

Based on described relationships, You can enumerate each property inside ExcelExport class like below :

NumberOfComment is equal to article.NumberOfComment for each article entry! Unless you employ another table named ArticleComment and take advantage of navigation property inside Article class (using public virtual ICollection<ArticleComment> Comments { get; set;}), Then count the number of comments with article.Comments.Count().

NumberOfReviews is equal to article.Reviews.Count() for each article entry.

Reviews for each article can be some thing like the following:

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

It seems you must also add another property to your ExcelExport class to show ReviewersAvaragePoint and enumerate that like this:

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

Edit based on OP's edit

By employing a List of ArticleReviewReport (e.g. List<ArticleReviewReport> Reviews) you have a flexible array (dynamic columns) to present in corresponding format. The missing part is making dynamic columns based on Distinct ReviewerId extracted from the ArticleReview table. Something like the following for entire articles:

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

Now you can assign each ArticleReviewReport to the corresponding column. Using a List<Dictionary<string, string>> would be a good data type for Reviews member.




回答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
     });
}

You also have to .Include(e => e.Reviews) if you use lazy loading.



来源:https://stackoverflow.com/questions/53692304/convert-a-table-with-different-relational-values-to-excel-columns

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!