-
Notifications
You must be signed in to change notification settings - Fork 269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LoadFromCollection enhancements #1058
Comments
@swmal The only item not listed that I can remember is that I'd like a runtime way of formatting. For example, when a column is a |
@grosch-intl Can you extend this class and show how you would like this to work? I assume the public class Actor
{
[EpplusIgnore]
public int Id { get; set; }
[EpplusTableColumn(Order = 3)]
public string LastName { get; set; }
[EpplusTableColumn(Order = 1, Header = "First name")]
public string FirstName { get; set; }
[EpplusTableColumn(Order = 2)]
public string MiddleName { get; set; }
[EpplusTableColumn(Order = 0, NumberFormat = "yyyy-MM-dd", TotalsRowLabel = "Total")]
public DateTime Birthdate { get; set; }
[EpplusTableColumn(Order = 4, NumberFormat = "€#,##0.00", TotalsRowFunction = RowFunctions.Sum, TotalsRowNumberFormat = "€#,##0.00")]
public double Salary { get; set; }
[EpplusTableColumn(Order = 5, NumberFormat = "0%", TotalsRowFormula = "Table1[[#Totals],[Tax amount]]/Table1[[#Totals],[Salary]]", TotalsRowNumberFormat ="0 %")]
public double Tax { get; set; }
} |
…s.DisplayAttribute in LoadFromCollection
@swmal Something like this: public class Example : Actor {
[EpplusTableColumn(Order = 6, ColumnFormatter = nameof(Formatter))]
public DateTime SomeDate { get; set; }
public void Formatter(ExcelTableColumn column) {
column.DataStyle.NumberFormat.Format = DateTimeFormatInfo.CurrentInfo.ShortDatePattern;
column.DataCellStyleName = _dateColumnStyle;
}
} |
* WIP on LoadFromCollection hyperlinks * Fixed an issue with auto filters * Fixed failing test * #1058, #946 added hyperlink style to hyperlinks created by the LoadFromCollection method --------- Co-authored-by: JanKallman <[email protected]> Co-authored-by: swmal <{ID}+username}@users.noreply.github.com>
@grosch-intl - does this look ok for #969? [TestClass]
public class LoadFromCollectionDictionaryPropertyTests
{
[EpplusTable]
public class TestClass
{
[EpplusTableColumn(Order = 2)]
public string Name { get; set; }
[EPPlusDictionaryColumn(Order = 1, ColumnHeaders = new string[] { "A", "B", "C"})]
public Dictionary<string, object> Dict { get; set; }
}
[TestMethod]
public void ShouldReadColumnsAndValuesFromDictionaryProperty()
{
var item1 = new TestClass
{
Name = "test 1",
Dict = new Dictionary<string, object> { { "A", 1 }, { "B", 2 } }
};
var items = new List<TestClass> { item1 };
using(var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
sheet.Cells["A1"].LoadFromCollection(items, c => c.PrintHeaders = true);
// Column headers
Assert.AreEqual("A", sheet.Cells["A1"].Value);
Assert.AreEqual("B", sheet.Cells["B1"].Value);
Assert.AreEqual("C", sheet.Cells["C1"].Value);
Assert.AreEqual("Name", sheet.Cells["D1"].Value);
// values
Assert.AreEqual(1, sheet.Cells["A2"].Value);
Assert.AreEqual(2, sheet.Cells["B2"].Value);
Assert.IsNull(sheet.Cells["C2"].Value);
Assert.AreEqual("test 1", sheet.Cells["D2"].Value);
}
}
} |
@swmal Unfortunately, no :( The headers aren't defined at compile time as you're showing there. The headers are going to be determined at runtime, so an attribute would tell which property contains the list of headers. [EpplusTable]
public class TestClass
{
[EpplusTableColumn(Order = 2)]
public string Name { get; set; }
[EPPlusIgnore]
public List<string> Headers { get; set; }
[EPPlusDictionaryColumn(Order = 1, ColumnHeaders = nameof(Headers))]
public Dictionary<string, object> Dict { get; set; }
} |
@grosch-intl Ok, I understand. I would, however, like to avoid to have the keys provided by another property on the class. We could read the keys/column headers from the first item in the list, but it doesn't feel intuitive. Not sure if I have understood your requirements fully, but see new suggestion below. I will keep the Let me know what you think. New interface IDictionaryKeysProvider in EPPlusnamespace OfficeOpenXml.LoadFunctions
{
/// <summary>
/// Provides keys of a property decorated with the <see cref="EPPlusDictionaryColumnAttribute"/>
/// </summary>
public interface IDictionaryKeysProvider
{
/// <summary>
/// This function will return keys that will be used as column headers
/// based on the <paramref name="key"/> that will be read from the <see cref="EPPlusDictionaryColumnAttribute"/>
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public IEnumerable<string> GetKeys(string key);
}
} New version of the above[EpplusTable]
public class TestClass
{
[EpplusTableColumn(Order = 2)]
public string Name { get; set; }
[EPPlusDictionaryColumn(Order = 1, HeadersKey = "1")]
public Dictionary<string, object> Columns { get; set; }
}
public class MyKeysProvider : IDictionaryKeysProvider
{
public IEnumerable<string> GetKeys(string key)
{
switch(key)
{
case "1":
return new string[] { "A", "B", "C" };
case "2":
return new string[] { "C", "D", "E" };
default:
return Enumerable.Empty<string>();
}
}
}
[TestMethod]
public void ShouldReadColumnsAndValuesFromDictionaryProperty()
{
var item1 = new TestClass
{
Name = "test 1",
Columns = new Dictionary<string, object> { { "A", 1 }, { "B", 2 } }
};
var items = new List<TestClass> { item1 };
using(var package = new ExcelPackage())
{
var sheet = package.Workbook.Worksheets.Add("test");
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.KeysProvider = new MyKeysProvider();
});
Assert.AreEqual("A", sheet.Cells["A1"].Value);
Assert.AreEqual("B", sheet.Cells["B1"].Value);
Assert.AreEqual("C", sheet.Cells["C1"].Value);
Assert.AreEqual("Name", sheet.Cells["D1"].Value);
Assert.AreEqual(1, sheet.Cells["A2"].Value);
Assert.AreEqual(2, sheet.Cells["B2"].Value);
Assert.IsNull(sheet.Cells["C2"].Value);
Assert.AreEqual("test 1", sheet.Cells["D2"].Value);
}
} |
@swmal That doesn't work because again, I'd have to know at compile time what my keys are. To make it work at runtime, I'd have to add a method to the keys provider to pass in the values for a given key, which breaks the single responsibility principle. I'm also not seeing where you're specifying that the table should use the Why don't you like the attribute? Seems clean and self-contained. Another potential option is we could use an |
@grosch-intl - I have looked at the code you added to #969 but since I don't know the structure of the Is this correct?
Anything else?
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.KeysProvider = new MyKeysProvider();
}); Then EPPlus will call the When EPPlus has retrieved this "initial delivery" of the keys (which also will be used as columns) it will loop through the list of objects and for each member that is decorated with the
Again, there might be a misunderstanding from my side here. But here are my objections to it based on my current understanding:
|
@swmal I'm so sorry, I thought I had replied to this!
No, there's no initial requirement. It's just a list of keys that will be determined at runtime: void Main() {
var keys = new HashSet<string>();
var rows = GetMyData();
foreach (var row in rows)
foreach (var date in row.CountByQuarter.Keys)
keys.Add(date);
var orderedColumnNames = keys.OrderBy(k => ...).ToArray();
sheet.Cells["A1"].LoadFromCollection(rows, c => {
c.PrintHeaders = true;
c.OrderedKeys = orderedColumnNames
});
}
public class Foo {
[EPPlusTableColumn(...)]
public string Something { get; set; }
public Dictionary<string, object> CountByQuarter { get; set; }
}
public List<Foo> GetMyData() { .... }
} So I got my data, then I looped through the rows and looked at the dictionary that was there, grabbing each unique key. Some rows might have some, some rows have others, some might have all. Then I sort that distinct list of keys via some business logic into the order that I want them to appear in the table. Now, when you write each row to the table, and it's time to hit the CountByQuarter property, you would create columns in the order specified via the I hope that's clearer?
At the point I call this, I'm always going to have that |
Ok, just so we are totally on the same page: you mean that there has to be no "master data" or "initial delivery" of the keys, but rather that EPPlus should loop through all the instances in the collection and create the dictionary with column names itself? Even if we have an ordered dictionary, autodetecting sorting will in some cases be impossible. T-shirt size is an obvious example (XS, S, M, L, XL), other domains could be various geographic structures, code systems, etc. So I think that we need to have the possibility for the user of our API to define the sort order themselves. Do you think the following would work?
sheet.Cells["A1"].LoadFromCollection(items, c =>
{
c.PrintHeaders = true;
c.DictionaryKeysSort = (headerKey, columns) => {
// re-sort columns...
return columns;
};
}); |
No. That's our job. When EPPlus is called I already have the sorted column names.
I'm realizing ordered dictionary wouldn't work because that would require ever row to have every key, which isn't the case.
I'm not exactly sure what that lambda is taking but I already have the sort order when I call LoadFromCollection, See my example where I set |
Trying to write this like an agile story: As a developer, I want to load my collection into a sheet using I will provide an array of header names to the load call, and when you go to generate cells for the dictionary property, regardless of the contents of the dictionary, you will create a column for each header provided in the array, in the same order as the array of header names. For each entry in the header array, check to see if that specific string exists as a key in the dictionary. If it does, use the provided value. If it does not, then leave the cell blank. Any key/value pairs that are in the dictionary, but are not present in the array of header names, should be ignored. |
Good, let's agree on the wording of this story first. To clarify what I did above I'm changing these two statements in the text:
As a developer, I want to load my collection into a sheet using LoadFromCollection. One or many of my properties to load might be a I will provide an array of header names for each such property to the load call, and when you go to generate cells for the dictionary property, regardless of the contents of the dictionary, you will create a column for each header provided in the array, in the same order as the array of header names. For each entry in the header array, check to see if that specific string exists as a key in the dictionary. If it does, use the provided value. If it does not, then leave the cell blank. Any key/value pairs that are in the dictionary, but are not present in the array of header names, should be ignored. |
Sounds good. |
If we use your code above, could this work? void Main() {
var keys = new HashSet<string>();
var rows = GetMyData();
foreach (var row in rows)
foreach (var date in row.CountByQuarter.Keys)
keys.Add(date);
var countByQuarterKeys = keys.OrderBy(k => ...).ToArray();
sheet.Cells["A1"].LoadFromCollection(rows, c => {
c.PrintHeaders = true;
c.RegisterDictionaryKeys(keysStore => {
// keysStore is something EPPlus will provide where you can map
// ordered keys (for example a string[]) to Dictionary properties
keysStore.RegisterKeys("cbq", countByQuarterKeys );
// you can continue to register more keys for other properties here...
}
});
}
public class Foo {
[EPPlusTableColumn(...)]
public string Something { get; set; }
[EPPlusDictionaryColumn(Order = 2, KeysId = "cbq")]
public Dictionary<string, object> CountByQuarter { get; set; }
}
public List<Foo> GetMyData() { .... }
} |
Yes, that would work, though it feels a bit verbose. On The normal case is going to just be a single dictionary like this. I've yet to want more than one, honestly. Even if somebody did want more than one, they could just call it again. But, if for whatever reason that doesn't work, yes, what you showed seems like what I need! |
We need to design our interfaces as futureproof as possible to avoid breaking changes later on. So we don't want to limit this to only support one Dictionary property even if that would be enough in your case. Your feedback is appreciated and I agree that we could make this less verbose. The reason for having the keyStore parameter would be if we believe that this will be a more complex domain in the future so we can extend it with more methods/properties. But that seems unlikely when I think about it. So when registring multiple sets of keys we can do as you suggested: sheet.Cells["A1"].LoadFromCollection(rows, c => {
c.PrintHeaders = true;
c.RegisterDictionaryKeys("cbq", countByQuarterKeys); We could also allow registering the keys with a default (internal) key that we would use if you skip setting the KeysId on the attribute. In that case you would get away with: void Main() {
var keys = new HashSet<string>();
var rows = GetMyData();
foreach (var row in rows)
foreach (var date in row.CountByQuarter.Keys)
keys.Add(date);
var countByQuarterKeys = keys.OrderBy(k => ...).ToArray();
sheet.Cells["A1"].LoadFromCollection(rows, c => {
c.PrintHeaders = true;
c.RegisterDictionaryKeys(countByQuarterKeys);
});
}
public class Foo {
[EPPlusTableColumn(...)]
public string Something { get; set; }
[EPPlusDictionaryColumn(Order = 2)]
public Dictionary<string, object> CountByQuarter { get; set; }
}
public List<Foo> GetMyData() { .... }
} |
Looks good. |
* #969, #1058 - implemented EPPlusDictionaryColumnAttribute for LoadFromCollection with attributes * #969, #1058 - added support for Dictionary property in LoadFromCollection * #1058, #969 - LoadFromCollection, support for Dictionary property * #1058, #969 - added testclass * #1058, #969 - added unit test for reading column headers/keys from attribute
@grosch-intl The Dictionary property and SortOrder on nested classes are done now and will be included in the next version (see table in the description of this issue). Here are unit tests that demonstrates how it works: |
Awesome, you rock! When do you think the next version will be released? |
Thanks - most likely within a week. If you want to test it you can use our dev nuget feed |
@grosch-intl we released new versions earlier today. |
@swmal Thank you so much, Mats! That really cleans up my code. Love how invested you guys are in continuing to make this product so useful for your customers. |
@grosch-intl Sorry for late response to this and thanks for your kind words. Just the formatting piece left, I am currently working on cleaning up some pieces related to #1196. If you have upgraded to EPPlus 7 the new Take and Skip functions can be useful for formatting columns. You could do something like this: var range = sheet.Cells["A1"].LoadFromCollection(items);
if(whatever)
{
range.TakeSingleColumn(3).Style.Numberformat = "#,###.00";
// or...
range.SkipRows(1).TakeColumnsBetween(3,2).Style.Numberformat = "#,###.00";
} |
@swmal Just checking in on this one. |
Hi Scott, I will pick up the remaining formatting functionality in the near future and hope to have it included in 7.1. |
Working on fixes for the following issues:
The text was updated successfully, but these errors were encountered: