I have to fill an excel like this
Where I have merged cells on each row.
I'm trying to use loadfromcollection, but it ignores the merged cells and it fills each single row.
Please did you know any quick way to do it?
edit:
I think that one way is to implement a custom loadfromcollection where the steps are:
Iterate collection
For each object, read properties/fields using reflection
Iterate cell on each row
check if range address is in worksheet.mergecells
fill it with property/field value
But in case of complex worksheet (with some loadfromcollection), I think that this approach could be heavy with a big cost.
edit 2:
Add an example:
Code like this
public class MyObj
{
public string first {get; set;}
public string second {get; set;}
}
...
List<MyObj> myList = new List<MyObj>() {
new MyObj() {first = "first1", second = "second1"},
new MyObj() {first = "first2", second = "second2"} };
...
ws.Cells["A1"].LoadFromCollection(myList);
In a normal worksheet, loadfromcollection has the following output:
If I merge column A and B after filled, I get this:
In case that my worksheet template is
and then I try to fill it using loadfromcollection I get this result
loadfromcollection ignores merged cell, and fill column A and B, but my expectation is
The problem is that this is an ´intended´ behavior in excel. When you have values in Columns A and B and you merge them, only the value in column A will remain. LoadFromCollection does not have an overload for this kind of scenario.
I can think of several ´workarounds´ which could work, depending on how general you want the code to be.
Your solution of iterating through object properties with reflection would work. If the ´merging´ is always two columns, I´d recommend using a column counter and add 2 to it each property iteration.
So something like this (where of course you already defined the excel package and worksheet etc and the start row and column):
var type = testObject.GetType();
var properties = type.GetProperties();
foreach (var property in properties)
{
cell = worksheet.Cells[row, column];
cell.Value = property.GetValue(testObject);
column += 2;
}
Alternatively, you could decide to remove the merging from the template and move it to your code instead. This may provide a little bit more consistency and flexibility as well.
Code above would then look more like:
var type = testObject.GetType();
var properties = type.GetProperties();
foreach (var property in properties)
{
cellRange = worksheet.Cells[row, column, row, column + 1];
cellRange.Merge = true;
cellRange.Value = property.GetValue(testObject);
column += 2;
}
You could even go a bit 'crazy' and make the range variable based on the property. You make a switch statement with all known properties which should be 2 merged cells, those which should be 3 merged cells etc. Then make the default 1 cell/column. This way you gain total control over the output and it's much easier to adjust in the future.
Edit
My case is a little bit more complex, not all my mergedcells are composed by 2 cells
Some code to illustrate my above statement with some code to help address this. Used the old switch notation as you said it was an 'old project' and I assume you aren't using C# 8 or higher yet.
var type = testObject.GetType();
var properties = type.GetProperties();
int columnMergeSize;
foreach (var property in properties)
{
columnMergeSize = GetPropertyColumnMergeSize(property.Name);
cellRange = worksheet.Cells[row, column, row, column + columnMergeSize];
cellRange.Merge = true;
cellRange.Value = property.GetValue(testObject);
column += 2;
}
private int GetPropertyColumnMergeSize(string propertyName)
{
switch (propertyName)
{
case "Property1":
case "Property2":
case "Property3":
case "Property4":
return 2;
case "Property5":
case "Property6":
return 3;
default:
return 1;
}
}
This will be less heavy than having to read out the properties of the cell each time to see if it's 'merged' or not. I've also found that if you remove nearly all formatting from an excel template it will load much faster speeding up your programme.
You can either decide to put this in a service and just inject the method into wherever it's needed with DI, or you can make it a static method and use it.
Related
I've tried to display the values of properties from a list of class on the table of MS Word like this:
public void CreateTable<T>(IList<T> list)
{
if (list != null)
{
var props = typeof(T).GetProperties();
Range tableLoc = WdDoc.Range();
var table = WdDoc.Tables.Add(tableLoc, list.Count, props.Length);
for (int i = 0; i < props.Length; i++)
{
table.Columns[i + 1].Select();
if (props[i].PropertyType == typeof(double))
{
WdApp.Selection.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphRight;
}
else
{
WdApp.Selection.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
}
}
//...
}
}
I tried to automatically set the text alignment of columns according to fill-in property type.
However, only the first row of each column can be set as the correct alignment I assign.
Other rows will always be set as the latest alignment of for loop.
Why did this happen? Did I miss any snippet?
Also, what are the differences between
WdApp.Selection.Paragraphs.Format.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
and
WdApp.Selection.Range.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphCenter;
Thanks!
The problem comes from using Selection.Range in this specific instance of working with the Word object model. When working with columns you need to use
WdApp.Selection.ParagraphFormat.Alignment
Background
Selection affects exactly what is selected - the entire column.
Selection.Range affects the Range of the selection. In the case of a Column, Selection.Range cannot be the entire column. A Range in Word must be a contiguous run of text, but a column is NOT contiguous. So Selection.Range returns only that part of the selection that is contiguous: the first cell (row). That explains why only the first row of the table is affected by your current code.
While a selected column appears to be contiguous this is just a convenience for the user. Under the covers, everything in the rows between the cells in one column and the next breaks up the text run.
If you're familiar with HTML think of how a table is defined in HTML. The way Word Open XML defines a table works on the same principle. The concept looks something like this:
<table><row><cell><cell></row><row><cell><cell><row></table>
The rows and their content are contiguous.
We've got problem with filtering for some of our columns in devexpress gridcontrol. We add the column dynamically (bound-type column) to the grid. The values from the source objects are long type. In the cells it seems that values are fine (since they're aligned to the right without any custom formating on our side) however in filter popup values behave like strings.
For example data set like 1,2,5,11,22,37 the filter list is sorted like 1,11,2,22,5,37 (just like strings) and when we choose one of the available values the filtering does not work (i mean, grid becames empty). Even filters like "Non empty cells" does not work, but when we choose "empty cells" only few from few thousand rows are shown even if most of the cells have no values.
It is important to point out that only dynamically added columns behave that way, the few columns we create every time our module runs work as intended.
The data source is a container (List like).
We're using DevExpress 13.2.
Example of creating 'custom column'
void CreateColumn(GridColumn gridColumn, string fieldName = null, string caption = null, bool visible = true,
bool readOnly = true, UnboundColumnType unboundType = UnboundColumnType.Bound,
int columnWidth = int.MinValue, int minColumnWidth = int.MinValue)
{
gridColumn.Caption = caption;
if (fieldName != null)
gridColumn.FieldName = fieldName;
gridColumn.Visible = visible;
gridColumn.OptionsColumn.ReadOnly = readOnly;
gridColumn.OptionsColumn.AllowEdit = !readOnly;
gridColumn.UnboundType = unboundType;
gridColumn.OptionsFilter.AllowAutoFilter = true;
gridColumn.FilterMode = ColumnFilterMode.Value;
gridColumn.OptionsFilter.AutoFilterCondition = DevExpress.XtraGrid.Columns.AutoFilterCondition.Contains;
if (columnWidth != int.MinValue)
{
gridColumn.Width = columnWidth;
gridColumn.OptionsColumn.FixedWidth = true;
}
if (minColumnWidth != int.MinValue)
gridColumn.MinWidth = minColumnWidth;
}
GridColumn gridColumn = new GridColumn();
CreateColumn(gridColumn, "someName", "someCaption", true, true);
View.Columns.Add(newGridColumn);
That's how it goes in our code (striped most of not related code just to give example process).
#edit
There's invalid cast exception when we add filter like this:
ColumnFilterInfo filter = GetFilter(); //cant really post code of this
ourGrid.MainView.ActiveFilter.Add(column, filter); // VS points here
Unfortunately it doesnt say what and where (except some 'lambda expression') exception is being thrown.
Of course column is the column mentioned above.
#edit
I've found new "tip". The FilterItem objects contain strings for sure, however they should contain long values. How can we influence the creation of these or atleast where to check why those are created like that (we dont do it manually)?
#Edit 19.11.2015
Alright, I had some breakthrough. The columns ('custom') thanks to our mechanism guess their type just fine. Then only problem is that infact our values which custom columns use are stored in Dictionary<string,object>-like collection and we think that thanks to PropertyDescriptor type for columns is fine, but for some reason FilterItem objects have Value of string. We belive that's because DX filter mechanism can't really guess type of "object" so it uses simple ToString on it so FilterItem.Value is string type instead column's data type.
How to overcome this?
We've found the solution and the mistake was on our side. Column creation etc is fine. Somewhere deep, someone has changed value types.
So, I have a DataGrid, that I want to manipulate programmatically a lot.
string[] values = new string[something.Count];
for (int i = 0; i < somethingElse.Count; i++)
{
if (condition)
values[i] = Data[i].ToString();
else
values[i] = "";
}
var style = new System.Windows.Style(typeof(DataGridRowHeader));
style.Setters.Add(new Setter(DataGridRowHeader.ContentProperty, Data[0].ToString()));
MyGrid.Items.Add(new DataGridRow()
{
HeaderStyle = style,
Item = values
});
This I do in a loop and I am able to fill in my grid with all the data I need.
Later, I am able to access cells, edit them, take their values, whatever I want and need.
However, when user wants to use the grid as you would in MS Excel, the cells are not editable.
So I went the other way and created a :
ObservableCollection<ObservableCollection<string>> gridData = new ObservableCollection<ObservableCollection<string>>();
//*** ... *** the gridData is filled in the same way, you can imagine
MyGrid.ItemsSource = gridData;
This does fill in the data perfectly the same way, more than that, the data are now editable.
But my custom row headers disappeared.
I need them, also, I do not think I want to use binding for row header values.
Can the first approach be somehow modified to still be editable, but rows with data being filled the very same way?
I eventually solved this combining both the abovementioned approaches.
First, I generate ObservableCollection<ObservableCollection<string>> dataCollection, fill it with data.
Next I generate an ObservableCollection<DataGridRow> rowCollection collection.
In my declarative part of the loop from the first approach, that you can see in the question, I do this
rowCollection.Add(new DataGridRow()
{
HeaderStyle = style,
Item = dataCollection[i] //for the corresponding iteration element
});
This all ends up when setting
MyGrid.ItemsSource = rowCollection;
Till now (very little time of testing), this seems to be what I was looking for, hope it might help someone else aswell.
I am able to apply NumberFormat on pivot fields using Microsoft.Office.Interop.Excel.PivotFields property. But that will not apply same format to summed fields. Is there any separate property for summed fields in pivot table that I am missing ?
Microsoft.Office.Interop.Excel.PivotFields pFields = pivotTable.PivotFields();
foreach (Microsoft.Office.Interop.Excel.PivotField pf in pFields)
if (pf.DataType == XlPivotFieldDataType.xlNumber)
pf.NumberFormat = "#,##0.00";
The documentation on these members isn't exactly crystal clear, but after a bit of digging around and trial and error it looks like the property you want is PivotTable.VisibleFields, which you need to cast to a PivotFields object to iterate through:
Excel.PivotFields vFields = (Excel.PivotFields)pivotTable.VisibleFields;
This will give you all visible fields in the table (as opposed to PivotTable.PivotFields() which only seems to give you the row labels column and the underlying source data). You might have to check the value of each one before you set the number format otherwise I believe it will apply it to everything in the table, which might not be what you want:
foreach (Excel.PivotField pf in vFields)
{
if (pf.DataType == Excel.XlPivotFieldDataType.xlNumber
& pf.Value == "Sum of Price")
{
pf.NumberFormat = "#,##0.00";
}
}
If you wanted to get to one cell in particular, then you can access the PivotField.PivotItems and iterate through those for each PivotField object:
Excel.PivotItems pi = (Excel.PivotItems)pf.PivotItems();
But I think most of the properties of PivotItem are read-only.
I have a datatable bound to a datagridview. However, the ordering of columns is messed up. I already made a column headers for each field put dataproperty name. I arranged it in the designer view. However, if i run the program column headers doesn't follow my arrangement. =_=. Does anybody know how to solve this....
EDIT::
I've Tried this approach. Is it Okay?
void SortDataGridViewColumns(DataGridView dgv)
{
var list = from DataGridViewColumn c in dgv.Columns
orderby c.Index
select c;
int i = 0;
foreach (DataGridViewColumn c in list)
{
c.DisplayIndex = i++;
}
}
***I've got this here but I use Index instead of Headertext. CASE CLOSED! LOL
I think you wil need to change the column order in runtime.
From MSDN:
When you use a DataGridView to display data from a data source, the columns in the data source's schema sometimes do not appear in the order you would like to display them. You can change the displayed order of the columns by using the DisplayIndex property of the DataGridViewColumn class.
You can change the order of the columns like this:
private void AdjustColumnOrder()
{
customersDataGridView.Columns["CustomerID"].Visible = false;
customersDataGridView.Columns["ContactName"].DisplayIndex = 0;
customersDataGridView.Columns["ContactTitle"].DisplayIndex = 1;
customersDataGridView.Columns["City"].DisplayIndex = 2;
customersDataGridView.Columns["Country"].DisplayIndex = 3;
customersDataGridView.Columns["CompanyName"].DisplayIndex = 4;
}
http://msdn.microsoft.com/en-us/library/wkfe535h.aspx#Y0
If you're going t use it frequently, I recomend you to make use of extension methods to add sintactic sugar and make it easy to read and maintain.