Opening PDFs in WebView2 based on selection in CheckBoxColumn - c#

So,
In my WPF application, I want my users to be able to open previews of invoices, so that they may either verify or discard them. I am letting them check rows (each row representing a invoice) in a DataGridCheckBoxColumn in my DataGrid, then clicking a button (which runs my CreateInvoicePreview() method, see bottom of post), having all of the invoice previews be opened in new windows (one window for each invoice).
Well.. What happens now, is: User checks InvoiceA and InvoiceB. Two invoices are opened, but they are the same: InvoiceC. The correct amount of invoices are always opened, but not the correct instance. If I open the temp folder specified in my file path, I see that all invoices in the datagrid has been saved: InvoiceA through InvoiceJ.
Let me take you through the code.
This is the method that creates that builds and saves the actual PDF's, which the WebView2 control uses as source, so that it can display them in-app. It is heavily abbreviated.
I have kept the structure with the nested foreach loops in case that is relevant.
public void CreatePreviewInvoice() {
/* SQL SERVER CODE
* SQL SERVER CODE
* SQL SERVER CODE */
List<PaidTrip> paidTrips = PaidTrips.ToList();
tripsGroupedByCompany = paidTrips.GroupBy(pt => pt.LicenseHolderID);
foreach (IGrouping<string, PaidTrip> companyGroup in tripsGroupedByCompany) {
/* SQL SERVER CODE
* SQL SERVER CODE
* SQL SERVER CODE */
List<LicenseHolder> licenseHolders = LicenseHolders.ToList();
IEnumerable<IGrouping<string, PaidTrip>> groupedByVehicle = companyGroup.GroupBy(n => n.VehicleID);
foreach (IGrouping<string, PaidTrip> vehicleGroup in groupedByVehicle) {
// Iterating over all data pertaining to each vehicle
foreach (PaidTrip trip in vehicleGroup) {
}
try {
string userName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\')[1];
string fileName = $"FORHÅNDSVISNING - MÅ IKKE SENDES! {LicenseHolderID + "_" + "Faktura_" + InvoiceID}.pdf";
string filePath = $#"C:\Users\{userName}\AppData\Local\Temp\";
PdfFilePath = $"{filePath}{fileName}";
//if (LicenseHolderID == PreviewInvoiceViewModel.SelectedRow.LicenseHolderID) {
document.Save($"{PdfFilePath}");
//} else {
// return;
//}
} catch (Exception ex) {
MessageBox.Show(ex.Message);
}
}
}
As you see, towards the end of the method I have commented out a bit of code, which was me trying to implement a way to filter based on the checked rows only. It did not work.
This is the XAML for the WebView2:
<Wpf:WebView2
x:Name="wv_preview_invoice" Loaded="{s:Action CreatePreviewInvoice}"
Height="997" Width="702" Canvas.Left="20" Canvas.Top="71"
Source="{Binding PdfFilePath}"></Wpf:WebView2>
PdfFilePath is a property, which is referenced within the method above.
It's given a value within the method, and when Source (for the WebView2) is called, it is able to get the value from PdfFilePath, and thus display the PDF.
But as I said initially, it just creates X amount of instances/windows of the same invoice. Always the same one, because of in what order they are queried from the database.
And finally, here is the method that run when they click whichever invoices they want to preview, it's to open the new window with the WebView2 control:
public void PreviewInvoices() {
bool success = false;
foreach (PaidTrip item in PaidTrips) {
if (item.IsChecked == true) {
ShowPreviewInvoiceDetailed(item);
success = true;
}
}
if (!success) {
MessageBox.Show("You must chose an invoice to preview first.");
}
}
The method that opens the next window where the WebView2 is, looks like this:
public void ShowPreviewInvoiceDetailed() {
PreviewInvoiceDetailedViewModel viewModel = new(windowManager);
windowManager.ShowWindow(viewModel);
}
What part (or several parts) of the picture am I missing?

I managed to solve this by doing the following:
I made a property; public static string PreviewedInvoice { get; set; } in the ViewModel of the parent window. In my method that opens the child window (where the preview invoices are to be displayed) I bind it to LicenseHolderID of the rows that have a checked CheckBox, via foreach loop, like such:
public void PreviewInvoices() {
bool success = false;
foreach (PaidTrip item in PaidTrips) {
if (item.IsChecked == true) {
PreviewedInvoice = item.LicenseHolderID;
ShowPreviewInvoiceDetailed();
success = true;
}
}
if (!success) {
MessageBox.Show("You must chose an invoice to preview first.");
}
}
So now I have a way to filter only the checked rows, and make sure only the LicenseHolderID which match those in the row with a checked CheckBox, are actually saved. I updated my main method:
if (LicenseHolderID == PreviewInvoiceViewModel.PreviewedInvoice) {
document.Save($"{fullPath}");
SourcePath = fullPath;
}
And I bound SourcePath to the the source of the WebView2 in the XAML.
I feel like this is a clunky way of doing it, and I am going back and forth between layers, as a comment (since removed) mentioned.
If anyone can show me a better way, I'm all ears..

Related

I want to export single row on button click from the gridview in c# winform

I am able to export complete gridview to pdf but I can't understand how to target the specific row and export it to pdf using itextsharp when button clicked
Here is my code below for exporting to pdf where I am able to export complete gridview
private void gvSamplereports_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == gvSamplereports.Columns["btnPDFsingle"].Index)
{
DateTime PrintTime = DateTime.Now;
if (gvSamplereports.Rows.Count > 0)
{
SaveFileDialog sfd = new SaveFileDialog();
sfd.Filter = "PDF (*.pdf)|*.pdf";
sfd.FileName = "SampleDataReports_" + PrintTime.ToShortDateString() + ".pdf";
bool fileError = false;
if (sfd.ShowDialog() == DialogResult.OK)
{
if (File.Exists(sfd.FileName))
{
try
{
File.Delete(sfd.FileName);
}
catch (IOException ex)
{
fileError = true;
MessageBox.Show("It wasn't possible to write the data to the disk." + ex.Message);
}
}
if (!fileError)
{
try
{
PdfPTable pdfTable = new PdfPTable(gvSamplereports.Columns.Count);
pdfTable.DefaultCell.Padding = 3;
pdfTable.WidthPercentage = 100;
pdfTable.HorizontalAlignment = Element.ALIGN_CENTER;
//Below line is to add the header column name on each page of pdf
pdfTable.HeaderRows = 1;
foreach (DataGridViewColumn column in gvSamplereports.Columns)
{
Font fon = FontFactory.GetFont("ARIAL", 6);
fon.SetStyle(1);
PdfPCell cell = new PdfPCell(new Phrase(column.HeaderText, fon));
cell.HorizontalAlignment = Element.ALIGN_CENTER;
pdfTable.AddCell(cell);
}
foreach (DataGridViewRow row in gvSamplereports.Rows)
{
foreach (DataGridViewCell cell in row.Cells)
{
Font fon = FontFactory.GetFont("ARIAL", 6);
PdfPCell cell2 = new PdfPCell(new Phrase(cell.Value?.ToString(), fon));
cell2.HorizontalAlignment = Element.ALIGN_CENTER;
pdfTable.AddCell(cell2);
//pdfTable.AddCell(cell.Value.ToString());
}
}
using (FileStream stream = new FileStream(sfd.FileName, FileMode.Create))
{
Document pdfDoc = new Document(PageSize.A4, 30f, 30f, 100f, 50f);
PdfWriter writer = PdfWriter.GetInstance(pdfDoc, stream);
//PDFFooter is class created for adding header and footer in the pdf
writer.PageEvent = new PDFFooter();
pdfDoc.Open();
pdfDoc.Add(pdfTable);
pdfDoc.Close();
stream.Close();
}
MessageBox.Show("Data Exported Successfully !!!", "Info");
}
catch (Exception ex)
{
MessageBox.Show("Error :" + ex.Message);
}
}
}
}
else
{
MessageBox.Show("No Record To Export !!!", "Info");
}
}
}
I have added image for reference, Once I click the button I want to export that single row with header columns name in pdf using Itextsharp in c# winform, the data exported in pdf should look like Image below
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.

WPF DataGrid populates after window loaded but how to select specific row, after load

So I have DataGrid with DataContext set to a DataTable, and is loaded via FillJobsGrid() when Window_Loaded. This works OK, but I wanted to be able to select a specific item when launching via a command line, the args part works without an issue, just 2 args with a switch and a value. The value is the Id of a row in the JobsGrid, what I cant see is how to select this row. before running BackUpSelectedClicked()
For inspiration amongst many other pages I looked here but which ever way I approach this I have never got a datagrid.Items.count. I guess this is about which event to run my code from.
Before the mod I had:
private void FillJobsGrid()
{
using (MySqlClientWrapper db = new MySqlClientWrapper(MyConnString))
{
string sql = "SELECT * FROM backupjobs";
SetJobsTable(db.GetDataTable(sql));
JobsGrid.DataContext = GetJobsTable();
}
}
The code I was hoping would do the trick was:
if (RunFromCommandLine)
{
foreach (DataRowView job in JobsGrid.Items)
{
if (job != null)
{
DataRow jobRow = job.Row;
long id = Convert.ToInt32(jobRow["Id"]);
if (Convert.ToInt32(CommandLineJobId) == id)
{
JobsGrid.SelectedItem = job;
}
}
}
BackUpSelectedClicked();
}
I have tried inserting it in FillJobsGrid, and placing it in DataGrid.DataContextChanged, Initialised, Loaded etc. Any help appreciated.

How to display code modules(.cs) as items in a listbox?

I'm currently working on a new GUI that has a listbox as a crucial element in it. So far I've managed to display and select multiple items in this listbox, which wasn't that big of a deal.
My goal is to have several .cs files(maybe in further expansion als vb script files) in a folder within the project, which are properly displayed in the main view listbox and will be executed if the corresponding item in the listbox is selected.
So far I've tried to build the listbox and all the other GUI stuff (buttons, text,...) and connected the listbox with a bindable collection of a script model(which is a own class for testing purposes at the moment and should be replaced with the correct .cs files)
In the code below you can see the work around with this custom class and the selection check for multiple listbox items.
private void Run_Click(object sender, RoutedEventArgs e)
{
//Show user the scripts which are being processed, these are the previous selected Scripts
List<string> selectedList = new List<string>();
foreach (ScriptModel selectedScript in MainListBox.SelectedItems)
{
selectedList.Add(selectedScript.Name.ToString());
}
//check if no Script was selected, and if so, just return/do nothing
if (selectedList.Count() == 0) { return; }
MessageBox.Show("The following Scripts will be processed: " + Environment.NewLine +
string.Join(Environment.NewLine, selectedList));
//Call the Connection for Data-Export
}
private BindableCollection<ScriptModel> _scriptscollection=new BindableCollection<ScriptModel>();
public BindableCollection<ScriptModel> ScriptsCollection
{
get { return _scriptscollection; }
set { _scriptscollection = value; }
}
I would like to know, how I can replace(or connect) these custom class with actual .cs files (which are some kind of scripts) in a project folder, so that I can display these file names and select the corresponding files for execution. (so the connection should work both ways)
I'm sorry if this question seems a bit weird and general, but I'm really confused about that.
I believe you have over complicated the matter. Here is the code that will find all of the .cs files in a directory and then upon selecting one in the ListBox will start that file.
It's hard to tell exactly what you're asking for but hopefully this helps.
XAML
<ListBox ItemsSource="{Binding ScriptFiles}" SelectedItem="{Binding SelectedScript}"/>
Code behind / ViewModel
public List<string> ScriptFiles => Directory.GetFiles(FilePath, "*.cs").ToList();
private string selectedScript;
public string SelectedScript
{
get { return selectedScript; }
set { selectedScript = value; Process.Start(value); }
}

Why can't I save my AppointmentsSearchEventArgs to variable?

I have a rather simple problem (I bealive). I have this method:
void appointments_SearchCompleted(object sender, AppointmentsSearchEventArgs e)
{
if (e.Results.Count() == 0)
{
results = "no events for the selected day";
//MessageBox.Show(results);
}
else
{
results = e.Results.Count() + " events found";
sourceItem = e.Results;
//MessageBox.Show(results);
}
}
And I can't "save" both results and sourceItem variables(which are class fields).
The Message box inside this method shows everything correct, howerver, on the outside results reverts to the default value.
The answer is simple: I didn't get how ansynch download works.
In my case I had to create a reference to my MainPage class, and bind items in appointments_SearchCompleted method.
Simple as that.

Keeping track of 40+ control values

I am in need of some guidance for the following design.
I have a tab control that contains various group boxes. Within the group box, there are specific controls that relates to that group box. For example:
Now whenever a change is made to any control in the group box, the value for the control needs to be tracked because at the end of the application run cycle, the control data will need to be saved to a file that contains that value. An example file is:
HOT_STANDBY_FEATURE_ENABLE [val from control here]
HEART_BEAT_DIGITAL_OUTPUT [val from control here]
....
A design that I have in mind has another that has just properties that the group box form sets whenever a ValueChanged event occurs on a control.
Example code:
class ConfigurationValues
{
public int HotStandbyValue { get; set; }
public int HeartBeatDigitalOutputValue { get; set; }
//...add all other controls here
}
The downside that I see to this is that there are 40 controls on that tab page, so I'd have to manually type each property. When the file needs to be generated at the end of the application run cycle, I have a method that gets the value of the control need.
Example:
private void GenerateFile()
{
string[] file =
"HOT_STANDBY_FEATURE_ENABLE " + ConfigurationTabSettings.HotStandbyValue;
}
Another design consideration I need to make is that whenever a user clicks "Open Configuration File", the values from the file from disk need to be loaded into the properties so the form can take that data on startup and populate the controls within the group boxes with their respective values.
Any suggestions on this design would be greatly appreciated. I know this is not the most efficent way to do this and am not the most experienced programmer, so any Google keywords I can search for would be great also.
You could xml serialise and xml deserialise your ConfigurationValues class rather than writing manual "generate file" and "read file" methods
http://support.microsoft.com/kb/815813
You'll need to bind the controls Text or Value properties to the properties in your ConfigurationValues class e.g.
ConfigurationValues cv = Repository.ReadConfigValues();
numPulseFilterDelay.DataBindings.Add("Value", cv, "PulseFilterDelay");
// Add the rest of your control bindings here
on the btnSave_Click() of your Form, end the current edit on the form and save the config values
void btnSave_Click(object sender, EventArgs e)
{
BindingContext[cv].EndCurrentEdit(); // Commits all values to the underlying data source
Repository.SaveConfigValues(cv);
}
In your repository class you'll need methods to Load() and Save() the data. You can put XmlSerialization code in here, or write your own format (depending on your requirements)
public class Repository
{
public static ConfigurationValues LoadConfigValues()
{
var cv = new ConfigurationValues();
string[] lines = File.ReadAllLines("values.cfg");
foreach (string cfg in lines)
{
string[] nameValue = cfg.Split(new char[] { ' ' } ); // To get label/value
if (nameValue[0] == "HOT_STANDBY_FEATURE_ENABLE")
cv.HotStandbyFeatureEnable = nameValue[1];
else if (nameValue[0] == "SOME_OTHER_PROPERTY")
cv.SomeOtherProperty = nameValue[2];
// Continue for all properties
}
return cv;
}
public static void SaveConfigValues(ConfigurationValues cv)
{
var builder = new StringBuilder();
builder.AppendFormat("HOST_STANDBY_FEATURE_ENABLE {0}\r\n", cv.HostStandbyFeatureEnable);
// Add the rest of your properties
File.WriteAllText("values.cfg", builder.ToString());
}
}

Categories

Resources