Home > Back-end >  I want to export single row on button click from the gridview in c# winform
I want to export single row on button click from the gridview in c# winform

Time:11-01

the data exported in pdf should look like Image below

CodePudding user response:

Separate the data from how you display it

In modern programming there is a tendency to separate the data (=model) from the way it is communicated to the operator (=view). This has the advantage that you can reuse the model if you decide to display it differently, for instance, if you want to show the data as a Graph, instead of a table.

To match the model with the view, an adapter class is needed. This adapter class is usually called the Viewmodel. Together the three classes are abbreviated MVVM. Consider to read some background information about this.

When using Winforms and DataGridView, people tend to fiddle directly with the Rows and the Cells, instead of separating the data from the way it is displayed. Quite often this leads to a lot of problems. Apart from that you can't unit test the data without the form, you can't reuse the data in other forms, nor change the data without having to change the DataGridView.

Winforms supports MVVM using property DataGridView.DataSource.

How to easily and efficiently access the data of your DataGridView?

Alas, you forgot to tell us what's in your DataGridView, and from your code I can't extract what is shown. So for the example, let's suppose you show several properties of a collection of Products:

class Product
{
    public int Id {get; set;}
    public string ProductCode {get; set;}
    public string Description {get; set;}
    public ProductType ProductType {get; set;}  // some enumeration: food / non-food / etc
    public decimal Price {get; set;}
    public int LocationId {get; set;}   // foreign key to Location table  
    ...
}

You probably don't want to show all properties.

So of course you have a procedure to fetch the products that you want to show initially:

IEnumerable<Product> FetchProductsToShow() {...}

Implementation is out of scope of the question.

Using Visual Studio Designer you have added a DataGridView, and one DataGridViewColumn per Product property that you want to show. You'll have to define which DataGridViewColumn will show the values of which property. This can be done using the designer. I usually do it in the constructor with the use of nameof.

public MyForm : Form
{
    InitializeComponents();

    // Define which column shows which Property:
    coilumnProductId.DataPropertyName = nameof(Product.Id);
    columnProductCode.DataPropertyName = nameof(Product.ProductCode);
    columnProductPrice.DataPropertyName =  nameof(Product.Price);
    ...

The advantage of using nameof, is that if later you decide to change the name of the properties, it is automatically changed here. Typing errors are detected by the compiler.

Now to show all Products, all you have to do is assign the ProductsToDisplay to the `dataGridView1.DataSource:

this.dataGridView1.DataSource = this.FetchProductsToShow().ToList();

And presto your data is shown.

However, if the operator edits the table, the data is not updated. If you want that, you'll have to put the products that must be shown into an object that implements IBindingList. Luckily there is already such a class, not surprisingly named BindingList<T>

Add the following property to your form:

public BindingList<Product> DisplayedProducts
{
    get => (BindingList<Product>)this.dataGridView1.DataSource;
    set => this.dataGridView1.DataSource = value;
}

Now all changes made by the operator are automatically updated in the BindingList: changes to cells, but also added and deleted rows.

private void ShowInitialProducts()
{
    this.DisplayedProducts = new BindingList<Product>(this.FetchProductsToDisplay().ToList());
}

To access the edited table, for instance after the operator pressed the OK button:

public void OnButtonOk_Clicked(object sender, ...)
{
    BindingList<Product> editedProducts = this.DisplayedProducts;
    // find out which products are changed, and process them:
    this.ProcessEditedProducts(editedProducts);
}

Back to your question

but I can't understand how to target the specific row

BindingList<T> does not implement IList<T>. The designers didn't find it useful to access this.DisplayedProducts[4] directly. After all: if the operator can rearrange the rows, you don't know what's in the row with index [4].

However, you might want to access the Products as a sequence. Therefore ICollection<T> is implemented.

If you want to access the current row, or the selected rows, consider to add the following properties to your form:

public Product CurrentProduct => this.dataGridView1.CurrentRow?.DataBoundItem as Product;

This will return the current Product, or null if nothing is selected

public IEnumerable<Product> SelectedProducts = this.dataGridView1.SelectedRows
    .Cast<DataGridViewRow>()
    .Select(row => row.DataBoundItem)
    .Cast<Product>();

So to access the selected Products after the operator pressed the Ok button:

public void OnButtonOk_Clicked(object sender, ...)
{
    IEnumerable<Product> selectedProducts = this.SelectedProducts;
    // process the selected products:
    this.ProcessProducts(selectedProducts);
}

There's room for improvement

If I look at your code, it seems to me that if the operator clicks on a cell in the column with name btnPDFsingle (why not use a name that explains what the columns displays?), then you do several things:

  • you ask the operator to provide a file name,
  • if the file exists, you delete it (and solve the problem if it can't be deleted)
  • then you create a PdfPTable and fill it with the contents of the DataGridView
  • Finally you write the PdfPTable to a file.

And you decide to do that all in one procedure. This makes it difficult to unit test it. You can't reuse any part of this code, and if you change part of this, it is difficult to detect which parts of your code also has to change.

private void gvSamplereports_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
    if (e.ColumnIndex == gvSamplereports.Columns["btnPDFsingle"].Index)
    {
        this.SaveProducts();
    }
    else
        ... // report problem to operator
}

private void SaveProducts()
{
    string fileName = this.AskOperatorForFileName();
    if (String.IsNullOrEmpty(fileName))
    {
        ... // report operator that no filename is given
        return;
    }

    // fetch the products that must be in the Pdf
    IEnumerable<Product> productsToSave = this.FetchProductsToSave();
    this.SaveProducts(fileName, productsToSave);
}

ICollection<Product> FetchProductsToSave()
{
    // Do you want to save all Products or Only the selected ones?
    return this.DisplayedProducts;
}

Note: if you decide to save something different, only the selected products, or maybe only the first 10 Products, or only the non-food products, all you have to do is change this method. The other methods don't know, and don't have to know which Products are saved.

By the way, did you notice, that until now I nowhere mentioned that the Products are saved as a PDF? If later you decide to save them as XML, CSV, or plain text, none of these procedures have to change.

private void SaveProducts(string fileName, IEnumerable<Product> productsToSave)
{
    PdfPTable tableToSave = this.CreatePdfPTable(productsToSave);
    this.SavePdfPTable (fileName, tableToSave);
}

private PdfPTable CreatePdfPTable(IEnumerable<Product> products)
{
    ...
    foreach (Product product in products)
    {
        ...
    }
}

Did you see, that to create the PdfPTable, I don't have to access the DataGridViewRows or Cells anymore? If you decide to change the layout of the PdfPTable, only this procedure is changed. No method outside knows anything about the inner format of the table. Easy to unit test, easy to reuse, easy to change.

private void SavePdfPTable (string fileName, PdfPTable pdfPTable)
{
    // if the file already exists, if will be overwritten (FileMode.Create)
    // so no need to delete it first
    using (FileStream stream = new FileStream(sfd.FileName, FileMode.Create))
    {
        ... etc, write the PdfTable in the stream.
    }
}

Did you see, that because of all the small procedures, each procedure has only one specific task. It is way easier to unit test this task, to replace this task with a similar task if you want a small change (save as XML for instance). You can reuse each of these procedure in different forms. Because you have proper unit tests, you don't have to be afraid that these procedures have unexpected behaviour.

Conclusion:

Don't make your procedures too big. Every Procedure should have one specific clear task. This is part of what is called separation of concerns. Consider to read some background information about this.

  • Related