I have a program exporting a spreadsheet, containing headers and some data. I need to protect the headers, but leave the data cells editable.
The problem is, upon setting the worksheet as protected, all the cells become read-only.
So, the approach I am using is to check each of the cells below the header to see if they are not empty, and unlock them if so.
public void formatSpreadsheet(OfficeOpenXml.ExcelWorksheet ws)
{
// autofit columns
for (int i = 1; i <= ws.Dimension.End.Column; i++)
{
ws.Column(i).AutoFit();
}
// protect headers/metadata
ws.Protection.IsProtected = true;
int row = HEADER_ROW_OFFSET;
int col = 1;
while (ws.Cells[row,col].Value != null)
{
while (ws.Cells[row,col].Value != null)
{
ws.Cells[row, col].Style.Locked = false;
col++;
}
row++;
}
}
Testing for null values like so:
if (ws.Cells[row,col].Value != null) ws.Cells[row,col].Style.Locked = false;
doesn't work.
I have also tried ToString() on the cell values, and it doesn't help.
Any ideas?
Your problem isn't the locking, but your loop that goes through the cells:
while (ws.Cells[row,col].Value != null)
As soon as it hits an empty cell, it will immediately exit the block, and nothing else will be performed.
The following should work fine:
// Lock the worksheet. You can do this here, or in the end. Doesn't really matter.
ws.Protection.IsProtected = true;
// Assuming `HEADER_ROW_OFFSET` is the first row that's not a header,
// we first define a "data" range as starting from the first column of that row,
// to the very last used row & column.
var dataCells = ws.Cells[HEADER_ROW_OFFSET, 1, ws.Dimension.End.Row, ws.Dimension.End.Column];
// Now go through each cell in that range,
foreach (var cel in dataCells)
{
// and unlock when it has content.
if (cel.Value != null) cel.Style.Locked = false;
}
Locking the entire spreadsheet, automatically permanently locks the cells.
If you want to only have certain cells locked, go trough the spreadsheet and lock them (leaving the spreadsheet itself unlocked)
Related
I wrote a small method that will give me the headers of a table in excel:
private List<string> GetCurrentHeaders(int headerRow, Excel.Worksheet ws)
{
//List where specific values get saved if they exist
List<string> headers = new List<string>();
//List of all the values that need to exist as headers
var headerlist = columnName.GetAllValues();
for (int i = 0; i < headerlist.Count; i++)
{
//GetData() is a Method that outputs the Data from a cell.
//headerRow is defining one row under the row I actually need, therefore -1 )
string header = GetData(i + 1, headerRow - 1, ws);
if (headerlist.Contains(header) && !headers.Contains(header))
{
headers.Add(header);
}
}
return headers;
}
Now I got an Excel-table, where the first value I need is in cell A11 (or Row 11, Column 1).
When I set a breakpoint after string header = GetData(i + 1, headerRow - 1, ws);, where i+1 = 1 and headerRow - 1 = 11, I can see that the value he read is empty, which is not the case.
The GetData-Method just does one simple thing:
public string GetData(int row, int col, Excel.Worksheet ws)
{
string val = "";
val = Convert.ToString(ws.Cells[row, col].Value) != null
? ws.Cells[row, col].Value.ToString() : "";
val = val.Replace("\n", " ");
return val;
}
I don't get why this can't get me the value I need, while it works on every other excel table too. The excel itself is no different from the others. It's file extension is .xls, the data is in the same layout as in the other tables, etc
There are a few steps to getting this right. You need to know the dimensions of your table to know where the headers are. Your method hast two ways of knowing this: 1) passing the table Range to the method, or 2) giving the coordinates of a cell within the table (usually the top-left cell) and trusting the CurrentRegion property to do the job for you. The most reliable way would be the first as you will be explicitly telling the method where to look, but it'll require the consumer to figure out the address which isn't always straightforward. The CurrentRegion approach works fine too but note that if you have an empty column within your table range, it will only address until that empty column. Having said all that, you could have the following:
List<string> GetHeaders(Worksheet worksheet, int row, int column)
{
Range currentRegion = worksheet.Cells[row, column].CurrentRegion;
Range headersRow = currentRegion.Rows[1];
var headers = headersRow
.Cast<Range>() // We cast so we can use LINQ
.Select(c => c.Text is DBNull ? null : c.Text as string) //The null value of c.Text is not null but DBNull
.ToList();
return headers;
}
Then you can simply test if you're missing headers. The following code assumes the ActiveCell is a cell within the table Range, but you can change that easily to address a specific cell.
List<string> GetMissingHeaders(List<string> expectedHeaders)
{
var worksheet = App.ActiveSheet; //App is your Excel application
Range activeCell = worksheet.ActiveCell;
var headers = GetHeaders(worksheet, activeCell.Row, activeCell.Column);
return expectedHeaders.Where(h => headers.Any(i => i == h) == false).ToList();
}
I need to pass the value of a new cell.
this is because I need to make the same logic as in ItextSharp, that logic being that I need to be able to create a table only by adding cells.
Instead of adding rows and then adding the cells to the rows.
var cellValue = new Cell();
((Row)Rows.LastObject).Cells[i] = cellValue;
public void Add(PdfPCell cellValue)
{
MigraRow row = null;
if (currentRowCellIndex == Columns.Count || Rows.Count == 0)
{
row = this.AddRow();
currentRowCellIndex = 0;
}
currentRowCellIndex += cellValue.Colspan;
int cellCount = ((MigraRow)Rows.LastObject).Cells.Count;
int celIndex = cellCount == 0 ? 0 : cellCount - 1;
var cell = ((MigraRow)Rows.LastObject).Cells[celIndex];
cell = cellValue.Clone();
cell.Elements = cellValue.Elements;
//((MigraRow)Rows.LastObject).Cells[celIndex]. = cellValue.Borders.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Borders = cellValue.Borders.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Elements = cellValue.Elements.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Shading.Color = cellValue.BackgroundColor;
//((MigraRow)Rows.LastObject).Cells[celIndex].MergeRight = cellValue.Colspan;
//para = ((Paragraph)cell.Elements.LastObject).Clone();
}
this solution doesnt work because ...Cells[i]... is read only.
but I need to do something akin to this, is it possible?
As you can see I know that I can pass the values in each property to the Cells[i], but I don't like that solution.
Ps. in the presented code the PdfPCell is just a MigraDoc cell with some added functions by me, it can be used as a regular MigraDoc cell.
Answer to the current question:
MigraDoc is not iTextSharp. You can either use MigraDoc as intended - or add a setter to overcome the "read only" limitation since MigraDoc is open source. Instead of creating a PdfPCell and changing that you could pass a reference to the cell created by MigraDoc to your code to change the contents. This should be a simple change.
Answer to the original question:
You don't have to call new Cell() - the cell will be created automatically when you call AddRow() for the table.
The code looks much nicer then without casts and such:
Row row = table.AddRow();
Cell cell = row.Cells[0];
Sample code:
http://pdfsharp.net/wiki/HelloMigraDoc-sample.ashx
I have exported an excel document. In that, I have to apply the background color of the entire row based on some conditions. For example, If I have 1000 lines in the excel, I want to apply the background in 100 rows only.
I tried to set the color with range values. I am able to apply a color based on that. But I couldn't apply particular rows alone.
objSHT.Range["A1: A11"].Interior.Color = ColorTranslator.ToOle((System.Drawing.Color)colorConverter.ConvertFromString("#97C2EC"));
Could you please provide me the solution to apply the color in an entire row (particular row - based on column value condition)?
Works perfectly :)
for (int z=1;z<30;z++)
{
Microsoft.Office.Interop.Excel.Range row = xlWorkSheet.Rows[z];
row.EntireRow.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Azure); ;
}
I was looking for that some time ago, now i figured out how to do it :D
var col2 = xlWorkSheet.UsedRange.Columns["I:I", Type.Missing];
foreach (Microsoft.Office.Interop.Excel.Range item in col2.Cells)
{
if (Convert.ToString(item.Value) == null)
{
//item.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Cyan);
item.EntireRow.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Cyan);
}
if (Convert.ToString(item.Value) != null)
{
if (Convert.ToString(item.Value) == "Afterbuild")
{
item.Value = "Afterbuild";
}
else
{
if (Convert.ToInt32(item.Value) < 0)
{
//item.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Yellow);
item.EntireRow.Interior.Color = System.Drawing.ColorTranslator.ToOle(System.Drawing.Color.Yellow);
}
}
}
}
So, based on conditions (values) in column "I" i do change color
I need some help with a for each statement, basically what happens is when a user edits a value within a cell, my foreach will apply this to all cells within the datagrid and change the value of them all, i need my foreach statement to work by iterating through the datagrid but only change the selected row that has been edited
try
{
//loop through each of the rows in the dgv
foreach (DataGridViewRow row in dgvDetials.SelectedRows)
{
int intQtyInsp = 0;
//if quantity inspected is empty:
if (row.Cells[2].Value.ToString() == "")
{
//quantity inspected is 0. Prevents null value errors:
intQtyInsp = 0;
}
intQtyInsp =
Int32.Parse(dgvDetials.CurrentRow.Cells[2].Value.ToString());
if (intQtyInsp < 0) // checks the cell for a negative value
{
intQtyInsp = 0; // if cells is negative submits value as Zero
}
else
{
//sets quantity inspected to value entered
intQtyInsp = Int32.Parse(row.Cells[2].Value.ToString());
}
if (intQtyInsp == 0) //if quantity inspected is 0. Ignore row.
{
}
else //else gather details and insert row as production.
{
area = dtArea2.Rows[0]["area_code"].ToString();
inspDate = dtpInspectionDate.Value.ToString("MM/dd/yyyy");
inspShift = cbShift.Text;
partNo = row.Cells[0].Value.ToString();
// dieCode = row.Cells[0].Value.ToString();
dieCode = "";
machine = "";
qtyInsp = intQtyInsp;
qtyInspRecorded = Int32.Parse(row.Cells[5].Value.ToString());
comment = "";
//machine = row.Cells[3].Value.ToString();
if (qtyInspRecorded == 0)
{
SQLMethods.insertProduction(area,
inspDate,
inspShift,
partNo,
dieCode,
qtyInsp,
comment,
machine);
}
else
{
SQLMethods.updateProduction(area,
inspDate,
inspShift,
partNo,
dieCode,
(qtyInspRecorded + qtyInsp),
comment,
machine);
}
}
}
retrieveData(); //reset values
}
catch (Exception ex)
{
MessageBox.Show(
"Error instering production values. Processed with error: "
+ ex.Message);
}
First of all, I would simplify the code here a little by splitting it into several methods that may be called from the For-loop. That would make it easier to read, and thereby easier to help you too. Just to provide an example, the following:
if (intQtyInsp < 0) // checks the cell for a negative value
{
intQtyInsp = 0; // if cells is negative submits value as Zero
}
else
{
//sets quantity inspected to value entered
intQtyInsp = Int32.Parse(row.Cells[2].Value.ToString());
}
could be replaced with something like:
int intQtyInsp = SetQuantityInspected();
Then that method could contain the if-structure. Repeat this for other parts of the code in the loop too. Trust me, this will make your life easier.
Also, it seems as if the result of this section is never used; the value of intQtyInsp is overwritten right afterwards!:
if (row.Cells[2].Value.ToString() == "")
{
//quantity inspected is 0. Prevents null value errors:
intQtyInsp = 0;
}
As for your question: I'm not sure how you would get the id of the row that is currently being edited. (If possible (?), it might be getter to loop through the table / data source behind the datagrid?).
In any case, what you need to do is something like the following inside your loop:
if(IsCurrentlyEditedRow(row)){
...
// (all the stuff currently in the body of your loop goes here)
...
}
Now you can implement the method IsCurrentlyEditedRow() to return True or False depending on whether or not the id of the current row is the the same as that of the one you are editing.
Sorry if this is not a very specific and detailed answer, hope it is of some use anyway.
I've got a grid (FlexGrid, from ComponentOne) in a Winform application and I'm trying to find a cell in that grid, given the cell's column index and its value.
I've written the extension method below to loop through the grid and find that cell.
I'm testing that method on a grid that has 6 columns and 64 rows. It took 10 minutes for my code to find the correct cell (which was on the last row)
Is there any way I can speed up my algorithm ?
Note: I've also tried setting PlayBack.PlayBackSetting.SmartMatchOption to TopLevelWindow, but it does not seem to change anything...
Thanks!
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue)
{
int count = table.GetChildren().Count;
for (int rowIndex = 0; rowIndex < count; rowIndex++)
{
WinRow row = new WinRow(table);
WinCell cell = new WinCell(row);
row.SearchProperties.Add(WinRow.PropertyNames.RowIndex, rowIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
if (cell.Exists)
return cell;
}
return new WinCell();
}
Edit
I modified my method to be like below(e.g. I don't use winrow anymore), this seems to be around 3x faster. It still needs 7 sec to find a cell in a table with 3 rows and 6 columns though, so it's still quite slow...
I'll mark this answer as accepted later, to leave time to other people to suggest something better
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
Playback.PlaybackSettings.SmartMatchOptions = Microsoft.VisualStudio.TestTools.UITest.Extension.SmartMatchOptions.None;
int count = table.GetChildren().Count;
for (int rowIndex = 0; rowIndex < count; rowIndex++)
{
WinCell cell = new WinCell(table);
cell.SearchProperties.Add(WinRow.PropertyNames.RowIndex, rowIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
if (cell.Exists)
return cell;
}
return new WinCell();
}
Edit #2:
I've tried using FindMatchingControls as per #Andrii's suggestion, and I'm nearly there, except that in the code below the cell's column index (c.ColumnIndex) has the wrong value..
public static WinCell FindCellByColumnAndValue2(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
WinRow row = new WinRow(table);
//Filter rows containing the wanted value
row.SearchProperties.Add(new PropertyExpression(WinRow.PropertyNames.Value, strCellValue, PropertyExpressionOperator.Contains));
var rows = row.FindMatchingControls();
foreach (var r in rows)
{
WinCell cell = new WinCell(r);
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
//Filter cells with the wanted value in the current row
var controls = cell.FindMatchingControls();
foreach (var ctl in controls)
{
var c = ctl as WinCell;
if (c.ColumnIndex == colIndex)//ERROR: The only cell in my table with the correct value returns a column index of 2, instead of 0 (being in the first cell)
return c;
}
}
return new WinCell();
}
I would suggest to perform direct looping through child controls - according to my experience searching controls with complicated search crieteria in Coded UI often works slow.
Edit:
To improve performance it is better to remove counting table's children and looping through rows. Also to avoid exception when finding control without row number you can use FindMatchingControls method, like in following:
public static WinCell FindCellByColumnAndValue(this WinTable table, int colIndex, string strCellValue, bool searchHeader = false)
{
Playback.PlaybackSettings.SmartMatchOptions = Microsoft.VisualStudio.TestTools.UITest.Extension.SmartMatchOptions.None;
WinCell cell = new WinCell(table);
cell.SearchProperties.Add(WinCell.PropertyNames.ColumnIndex, colIndex.ToString());
cell.SearchProperties.Add(WinCell.PropertyNames.Value, strCellValue);
UITestControlCollection foundControls = cell.FindMatchingControls();
if (foundControls.Count > 0)
{
cell = foundControls.List[0];
}
else
{
cell = null;
}
return cell;
}
When table field will be searched directly in the table it will save time for counting child controls in table. Also searching without for loop will save time for search of field for each iteration of row number that does not match.
As row number is iterated through all available values in your extension - it is not essential search criterion in the long run. In same time each iteration through a row number value invokes additional control search request - eventually multiplying method's execution time by the number of rows in grid.