问题
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 Artlicle
s. 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 Article
s. 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