I have an application which gets data from Excel Sheets using OleDb.
On the Form I have controls so the user can filter the data to his needs.
For example, FileSize, UserID, Rootpath etc. This works perfectly.
After final selection the User has to press an "update" Button so I can filter the data based on his input. The result will be shown in a DataGridView.
However, since the Data on the Excel Sheets varies a lot, I used to have a ProgressBar on a second Form (Waitform) or make the DataGridView invisible while the ProgressBar on the UI is visible during the non-UI-Task (Data Collection).
I do know that I should use a Task or a Thread (or a BackGroundWorker) to keep the UI responsive.
That being said, it still freezes my whole application.
//Update Button which uses all the userdefined filters
private async void updateButton_Click(object sender, EventArgs e)
{
WaitBarDatagrid.Visible = true; //Progressbar is called WaitBarDatagrid
WaitBarDatagrid.Style = ProgressBarStyle.Marquee;
WaitBarDatagrid.MarqueeAnimationSpeed = 30;
dataGridView1.Visible = false;
await Task.Run(() => QueryToExcel());
dataGridView1.DataSource = FileInfos;
WaitBarDatagrid.Visible = false;
dataGridView1.Visible = true;
}
private void QueryToExcel()
{
this.Invoke((MethodInvoker)delegate ()
{
string fSize;
if (FileSizeComboBox.Text == "All Data")
{ fSize = "0"; }
else if (FileSizeComboBox.Text == "> 1 MB")
{ fSize = "1000"; } // 1MB = 1000kB
else if (FileSizeComboBox.Text == "> 10 MB")
{ fSize = "10000"; } // 10MB = 10.000kB
else if (FileSizeComboBox.Text == "> 100 MB")
{ fSize = "100000"; } // 100MB = 100.000kB
else if (FileSizeComboBox.Text == "> 1 GB")
{ fSize = "1000000"; } // 1 GB = 1000.000 kB
else
fSize = "0";
// The following ensures that all possibilities of User Definition are covered
string user = "";
string size = "";
string sep = ""; //Seperator
if (!string.IsNullOrEmpty(UserTextbox.Text))
{
user = $"[UserID] = '{UserTextbox.Text}'";
sep = "AND";
}
if (!string.IsNullOrEmpty(FileSizeComboBox.Text))
{
size = $"{sep} [File Size] >= {fSize}";
sep = "AND";
}
//Final Where CLAUSE based on User Input
//string command = $#"{user} {size}{sep} [Date] <= {DateBox.Value.ToOADate()}";
string command = $#"{user} {size} {sep} [Date] <= {DateBox.Value.ToOADate()}";
//Call Data from Excel
string connectionString = GetConnectionString(Datapath + RootCombobox.Text);
string query = $#"SELECT * from [FileInfos$] WHERE ({command})";
DataTable dt = new DataTable();
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
conn.Open();
using (OleDbDataAdapter dataAdapter = new OleDbDataAdapter(query, conn))
{
try
{
dataAdapter.Fill(dt);
FileInfos = dt;
}
catch (System.Data.OleDb.OleDbException ex)
{
MessageBox.Show(ex.ToString());
}
}
}
});
}
So far I also have tried to assign the values of the Userinputs to global variables and it will be changed in their correspondent events. However, even with invoke my UI freezes. Where does it come from?
The QueryToExcel() method is supposed to queue the work to run on a ThreadPool Thread, to let UI Thread continue its own work without freezing.
But you notice that the UI freezes anyway, saying:
even with invoke my UI freezes
It's invoking the UI thread from another thread that freezes it.
Doing work on another thread is about not using the UI thread. If we invoke back the UI thread from a worker thread, the effect is lost (or partially lost, annoying in any case).
You're also using Invoke() instead of BeginInvoke(). The latter is executed asynchronously: it returns immediately and can prevent deadlocks if the control invoked is busy or otherwise unreachable/unresponsive.
It won't prevent the UI from stuttering at times, anyway.
Looking at the code you presented here, it appears that there's no need to invoke the UI thread at all: the secondary thread just needs the properties values of some controls and then assigns a DataTable to a field.
It is then possible to pass to this method the required values as arguments, assigning the Controls' properties to some variables or to the properties of a class (so it's easier to understand what the arguments contain).
the worker method could be changed in
private DataTable QueryToExcel(string[] paramArray)
Or
private DataTable QueryToExcel(SomeClass values)
and can be called as:
private async void updateButton_Click(object sender, EventArgs e)
{
var dt = await Task.Run(() => QueryToExcel(values));
Or
dataGridView1.DataSource = await Task.Run(() => QueryToExcel(values));
}
In QueryToExcel() to Excel:
Access the values parameter to setup the query or other processing.
Create the DB Connection and fill a DataTable/DataSet.
Dispose of all the disposable objects created (Connection/DataAdapter etc)
Return a DataTable
Your code basically jumps to a non-UI thread and then jumps back to the UI - it's almost as if you never left the UI thread.
What you need to do is do all of your UI work on the UI thread and only do non-UI work on the other thread.
Try this code:
// Define other methods and classes here
//Update Button which uses all the userdefined filters
private async void updateButton_Click(object sender, EventArgs e)
{
WaitBarDatagrid.Visible = true; //Progressbar is called WaitBarDatagrid
// WaitBarDatagrid.Style = ProgressBarStyle.Marquee;
// WaitBarDatagrid.MarqueeAnimationSpeed = 30;
dataGridView1.Visible = false;
string fSize;
if (FileSizeComboBox.Text == "All Data")
{ fSize = "0"; }
else if (FileSizeComboBox.Text == "> 1 MB")
{ fSize = "1000"; } // 1MB = 1000kB
else if (FileSizeComboBox.Text == "> 10 MB")
{ fSize = "10000"; } // 10MB = 10.000kB
else if (FileSizeComboBox.Text == "> 100 MB")
{ fSize = "100000"; } // 100MB = 100.000kB
else if (FileSizeComboBox.Text == "> 1 GB")
{ fSize = "1000000"; } // 1 GB = 1000.000 kB
else
fSize = "0";
// The following ensures that all possibilities of User Definition are covered
string user = "";
string size = "";
string sep = ""; //Seperator
if (!string.IsNullOrEmpty(UserTextbox.Text))
{
user = $"[UserID] = '{UserTextbox.Text}'";
sep = "AND";
}
if (!string.IsNullOrEmpty(FileSizeComboBox.Text))
{
size = $"{sep} [File Size] >= {fSize}";
sep = "AND";
}
//Final Where CLAUSE based on User Input
//string command = $#"{user} {size}{sep} [Date] <= {DateBox.Value.ToOADate()}";
string command = $#"{user} {size} {sep} [Date] <= {DateBox.Value.ToOADate()}";
await Task.Run(() => QueryToExcel(command, RootCombobox.Text));
dataGridView1.DataSource = FileInfos;
WaitBarDatagrid.Visible = false;
dataGridView1.Visible = true;
}
private void QueryToExcel(string command, string RootCombobox_Text)
{
//Call Data from Excel
string connectionString = GetConnectionString(Datapath + RootCombobox_Text);
string query = $#"SELECT * from [FileInfos$] WHERE ({command})";
DataTable dt = new DataTable();
using (OleDbConnection conn = new OleDbConnection(connectionString))
{
conn.Open();
using (OleDbDataAdapter dataAdapter = new OleDbDataAdapter(query, conn))
{
try
{
dataAdapter.Fill(dt);
this.Invoke((MethodInvoker)delegate () { FileInfos = dt; });
}
catch (System.Data.OleDb.OleDbException ex)
{
this.Invoke((MethodInvoker)delegate () { MessageBox.Show(ex.ToString()); });
}
}
}
}
It's untested, but it should be close. Note that non of the UI elements are accessed or updated on any non-UI thread.
Related
I have a button that after I click it send a lot of data in a remote database with a loop, but during this operation whole wpf UI is freezing. My goal is to make the loader work while it is processing everything with the database.
My button code:
private void btn_Start_Click(object sender, RoutedEventArgs e)
{
pb_loader.IsIndeterminate = true; //<- it has to start to make animation
IEmailService emailService = new EmailService();
IUserQueryService emailQueryService = new UserQueryService();
var idIniziale = int.Parse(txtIdIniziale.Text);
var idFinale = int.Parse(txtIdFinale.Text);
var n = idFinale - idIniziale;
string mail = "";
for(int i=0; i<=n; i++)
{
mail = txtMail.Text + idIniziale + "#mail.local";
var exist = emailQueryService.CheckUserExist(mail); //<- db operation method
if (exist == false)
{
var lastUniqueId = emailQueryService.GetLastUniqueId();//<- db operation method
lastUniqueId = lastUniqueId + 1;
var idUtente = emailService.SalvaUtente(mail, lastUniqueId); //<- db operation method
emailService.AssegnaReferente(idUtente, txtMail.Text);//<- db operation method
emailService.AssegnaRuoli(idUtente); //<- db operation method
}
idIniziale++;
}
pb_loader.IsIndeterminate = false; //<- it has to end animation of loading
}
One straighforward approach for running a background operation in an event handler is to declare the event handler async and run and await a Task:
private async void btn_Start_Click(object sender, RoutedEventArgs e)
{
// prevent click while operation in progress
btn_Start.IsEnabled = false;
pb_loader.IsIndeterminate = true;
// access UI elements before running the Task
var mail = txtMail.Text + idIniziale + "#mail.local";
...
await Task.Run(() =>
{
// perform background operation
// use local variables "mail" etc. here
});
pb_loader.IsIndeterminate = false;
btn_Start.IsEnabled = true;
}
I want to process a large ammount of data stored in a text file. Here is the code I use to make it work faster:
var result = File
.ReadLines(textBox1.Text)
.AsParallel()
.WithDegreeOfParallelism(100)
.Select(line => ProcessLine(line));
The method ProcessLine gets the line then processes it and add it to an ArrayList.
After all the processing is done I load the ArrayList into a Datagrid,
but sometimes it completes all the lines and sometimes it hangs, I don't know why.
Any suggestions ?
Update
Here is the Method ProcessLine
private string ProcessLine(string domain)
{
ProcessStartInfo cmdinfo = new ProcessStartInfo();
cmdinfo.FileName = "cmd.exe";
cmdinfo.Arguments = "/c nslookup";
cmdinfo.RedirectStandardInput = true;
cmdinfo.RedirectStandardOutput = true;
cmdinfo.CreateNoWindow = true;
cmdinfo.UseShellExecute = false;
cmdinfo.RedirectStandardError = false;
Process cmdd = new Process();
cmdd = Process.Start(cmdinfo);
string spf = "none";
createproc:
try
{
cmdd.StandardInput.WriteLine("set q=txt");
cmdd.StandardInput.Flush();
cmdd.StandardInput.WriteLine(domain);
cmdd.StandardInput.WriteLine("exit");
cmdd.StandardInput.WriteLine("exit");
StreamReader r = cmdd.StandardOutput;
//cmdd.WaitForExit();
cmdd.Close();
spf = "";
string rdl = string.Empty;
bool spffound = false;
while (rdl != null)
{
try
{
rdl = r.ReadLine();
if (rdl.Contains("v=spf"))
{
spffound = true;
spf = rdl.Trim();
this.Invoke(new MethodInvoker(delegate
{
textBox2.AppendText("domain found : " + domain + Environment.NewLine + "SPF = " + spf + Environment.NewLine);
textBox2.Update();
}));
break;
}
}
catch (Exception)
{
}
}
if (!spffound)
spf = "none";
nbrDoms++;
this.Invoke(new MethodInvoker(delegate
{
DomsElapsed.Text = nbrDoms + " Domains Elapsed";
DomsElapsed.Update();
}));
SPFRecord srx = new SPFRecord((string)spf.Clone(), (string)domain.Clone());
if (srx == null)
{
cmdd.Kill();
cmdinfo = new ProcessStartInfo();
cmdinfo.FileName = "cmd.exe";
cmdinfo.Arguments = "/c nslookup";
cmdinfo.RedirectStandardInput = true;
cmdinfo.RedirectStandardOutput = true;
cmdinfo.CreateNoWindow = true;
cmdinfo.UseShellExecute = false;
cmdinfo.RedirectStandardError = false;
cmdd = new Process();
cmdd.StartInfo = cmdinfo;
cmdd.Start();
goto createproc;
}
lock (pageManager)
{
pageManager.AddRecord(srx);
}
//this.Invoke(new MethodInvoker(delegate
//{
//}));
}
catch(Exception exc)
{
cmd.Kill();
cmdinfo = new ProcessStartInfo();
cmdinfo.FileName = "cmd.exe";
cmdinfo.Arguments = "/c nslookup";
cmdinfo.RedirectStandardInput = true;
cmdinfo.RedirectStandardOutput = true;
cmdinfo.CreateNoWindow = true;
cmdinfo.UseShellExecute = false;
cmdinfo.RedirectStandardError = false;
cmdd = new Process();
cmdd.StartInfo = cmdinfo;
cmdd.Start();
Thread.Sleep(10);
goto createproc;
}
return "";
}
Read the lines of text into a string, with something like file.readalllines(psudo code)
Basically each thread is locking the other, are you trying this for speed or because the file is too large to fit into memory?
Ok, a few things to mention:
Do not use goto statements - it's hard to understand what your method does. Just move the creation of the Process into a separate method and call that method instead of using goto
Processes do take time to load and quite a lot for what you want to do. To avoid this load penalty, instead of creating and calling a process try to replace that with a method which does the same. There's an example of nslookup without calling the process. Try adapting it to your needs
Remove the locks - if your application somehow gets to use 100 threads the lock is a waste of time. You'll have 99 threads waiting for the other single thread to push its data to the pageManager. As #Mrinal Kamboj pointed out, you can use a thread-safe collection. In this case, use a BlockingCollection<T> and add the results there. At the other end of the queue have the pageManager listening and consuming each item as it arrives.
The UI needs its separate cycles to refresh which also takes time. If pageManager.AddRecord() somehow has to refresh the UI then the other threads won't wait just for the add operation.
UI updates must be done in the thread that created the controls and that thread can't update the UI if it's waiting for another thread.
The overall algorithm should look like this:
public class Engine
{
private readonly BlockingCollection<string> _messagePipeline = new BlockingCollection<string>();
public BlockingCollection<string> MessagePipeline
{
get { return _messagePipeline; }
}
public void Process(string file)
{
File.ReadLines(file)
.AsParallel()
.ForAll(line =>
{
var nsLookupResult = NsLookupMethod(line);
if(nsLookupResult.HasInfoYouNeed)
_messagePipeline.Add(nsLookupResult.DisplayInfo);
});
}
}
public class MainForm : Form
{
private readonly Engine _engine; // ...
private void OnStartButtonClick(object sender, EventArgs e)
{
var cts = new CancellationTokenSource();
_engine.Process(textbox1.Text);
Task.Factory.StartNew(()=>
{
foreach(var message in _engine.MessagePipeline.GetConsumingEnumerable())
{
// show the message
Application.DoEvents(); // allow the app to process other events not just pushing messages.
}
}, cts.Token,
TaskCreationOptions.PreferFairness,
// Specify that you want UI updates to be done on the UI thread
// and not on any other thread
TaskScheduler.FromCurrentSynchronizationContext());
}
}
And that should do it. I do have a (more or less academic) example of this kind of logic in action. The UI update logic is in the MainForm of the application and the processing logic is in an Engine class; have a look there.
I want to synchronize my local and web database so i have written a stored procedure using linked server. My stored procedure executes fine and data synchronization is successful but the procedure takes around 7-10 minutes to get executed. The exact timing cannot be determined. So whenever the procedure runs on my windows application then the page seems as if it has become unresponsive though the process is still going on.
So i am having a "Data Sync" button on my page on click of which i want the progress bar to display the progress of the stored procedure. For the time being I am taking the average of last few execution timings to define the time duration for which the stored procedure runs. Now the problem is that when i click on the data sync button then the progress bar doesn't work. Kindly help me with this issue.
My code is as follows:-
namespace RMS
{
public partial class DataSync : Form
{
connection con = new connection();
SqlCommand cmd = new SqlCommand();
static int rowCount;
static int syncTime;
static int timeSlice;
public DataSync()
{
InitializeComponent();
}
private void btnDataSync_Click(object sender, EventArgs e)
{
// Start the asynchronous operation.
backgroundWorker1.RunWorkerAsync();
try
{
con.GetConnectLive();
con.GetConnect();
if (con.CnLive.State == ConnectionState.Open)
{
MessageBox.Show("Connection to Live Server Successful!!!...Data Synchronisation may take several minutes so do not cancel the operation while in execution mode");
btnDataSync.Enabled = false;
btnDataSync.Text = "Please Wait...";
string Str = "RMS_LocalToLive";
cmd = new SqlCommand(Str, con.Cn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 1200;
rowCount = cmd.ExecuteNonQuery();
if (rowCount > -1)
{
MessageBox.Show("Total no. of rows synchronised = " + rowCount);
btnDataSync.Text = "Success";
}
else
{
MessageBox.Show("Data Synchronisation couldn't be completed because of connection problem... Please try again!!!");
}
}
else
{
MessageBox.Show("Unable to connect to Live Server...Please check your internet connection and try again!!!");
}
con.GetDisConnect();
con.GetDisConnectLive();
}
catch (Exception ex)
{
MessageBox.Show("Please check your internet connection and try again!!!");
}
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
con.GetConnect();
string Str = "RMS_DataSyncTime";
cmd = new SqlCommand(Str, con.Cn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 1200;
syncTime = Convert.ToInt32(cmd.ExecuteScalar().ToString());
timeSlice = syncTime / 100;
con.GetDisConnect();
}
catch (Exception ex)
{
MessageBox.Show("Unable to retrieve last Data Synchronisation Timing");
}
for (int i = 1; i <= synctime; i=i+timeslice)
{
Thread.Sleep(timeslice);
// Report progress.
backgroundWorker1.ReportProgress(i);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// Change the value of the ProgressBar to the BackgroundWorker progress.
progressBar1.Value = e.ProgressPercentage;
// Set the text.
this.Text = e.ProgressPercentage.ToString() + "% Completed";
}
private void DataSync_Load(object sender, EventArgs e)
{
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgse)
{
}
}
}
The main issue here is that, while you are executing your progress bar updates in the BackgroundWorker's thread, the ReportProgress() updates never make it to the UI thread, because you've blocked that thread with the main SQL operation.
Instead of doing that, you should do something more like this:
private void btnDataSync_Click(object sender, EventArgs e)
{
// Start the asynchronous operation.
backgroundWorker1.RunWorkerAsync();
btnDataSync.Enabled = false;
btnDataSync.Text = "Please Wait...";
bool success = false;
try
{
// Execute the query asynchronously
success = await Task.Run(() => ExecuteLocalToLive());
}
catch (Exception ex)
{
MessageBox.Show("Please check your internet connection and try again!!!");
}
btnDataSync.Enabled = true;
btnDataSync.Text = success ? "Success" : "Failure";
}
private bool ExecuteLocalToLive()
{
bool success = false;
con.GetConnectLive();
con.GetConnect();
if (con.CnLive.State == ConnectionState.Open)
{
MessageBox.Show("Connection to Live Server Successful!!!...Data Synchronisation may take several minutes so do not cancel the operation while in execution mode");
string Str = "RMS_LocalToLive";
cmd = new SqlCommand(Str, con.Cn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.CommandTimeout = 1200;
rowCount = cmd.ExecuteNonQuery();
if (rowCount > -1)
{
MessageBox.Show("Total no. of rows synchronised = " + rowCount);
success = true;
}
else
{
MessageBox.Show("Data Synchronisation couldn't be completed because of connection problem... Please try again!!!");
}
}
else
{
MessageBox.Show("Unable to connect to Live Server...Please check your internet connection and try again!!!");
}
con.GetDisConnect();
con.GetDisConnectLive();
return success;
}
I have rearranged the code that handles the button state and text, so that it's still executed in the UI thread where it belongs, even though the method itself is not. You also never appeared to set the button back to the enabled state; it's not clear to me whether that was intentional or not, so I went ahead and added a line to do that.
Finally, I will strongly recommend you figure out a better way to report status to the user than the calls to MessageBox.Show() you have now. The biggest issue is that you don't even start doing any work until after the user dismisses the initial message, which immediately puts your progress bar out of sync with the actual work. But it's also better to keep all your UI in the UI thread, and to keep UI separate from non-UI logic (i.e. the SQL operation).
All, I have successfully used ADO.NET to make use of asynchronous SQL queries similar to the example below. In the example shown the method ExecNonQuery is being invoked from the UI thread. This works well, but I wondered how I would handle the callback if I were to call ExecNonQuery from a non-UI thread?
Note. Clearly, in such a case I would amend ExecNonQuery, so that such things as this.toolStripStatusLabel1.Text were dealt with accordingly, or removed.
public bool ExecNonQuery(string strCmd, string strUserMsg = "")
{
try
{
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandText = strCmd;
cmd.CommandTimeout = 0;
bIsExecuting = true;
AsyncCallback callback = new AsyncCallback(HandleCallback);
cmd.BeginExecuteNonQuery(callback, cmd);
return true;
}
catch (Exception Ex)
{
bIsExecuting = false;
this.toolStripStatusLabel1.Text = String.Format("Ready (last error: {0})", Ex.Message);
if (conn != null)
conn.Close();
}
return false;
}
private delegate void DisplayInfoDelegate(string Text);
private void HandleCallback(IAsyncResult result)
{
try
{
// Retrieve the original command object, passed
// to this procedure in the AsyncState property
// of the IAsyncResult parameter.
SqlCommand command = (SqlCommand)result.AsyncState;
int rowCount = command.EndExecuteNonQuery(result);
string rowText = " rows affected.";
if (rowCount == 1)
rowText = " row affected.";
rowText = rowCount + rowText;
// Call the procedure from the form's thread.
DisplayInfoDelegate del = new DisplayInfoDelegate(DisplayResults);
this.Invoke(del, rowText);
}
catch (Exception ex)
{
// Because you are now running code in a separate thread,
// if you do not handle the exception here, none of your other
// code catches the exception.
// You can create the delegate instance as you
// invoke it, like this:
this.Invoke(new DisplayInfoDelegate(DisplayResults),
String.Format("Ready(last error: {0}", ex.Message));
}
finally
{
bIsExecuting = false;
if (conn != null)
conn.Close();
}
}
private void DisplayResults(string Text)
{
this.toolStripStatusLabel1.Text = Text;
this.toolStripProgressBar1.Style = ProgressBarStyle.Blocks;
this.toolStripProgressBar1.Value = 100;
}
Thanks for you time.
It makes no difference to your callback which thread runs ExecNonQuery - HandleCallback will still be run on a thread pool thread.
You have already spotted the change you need to make: don't access UI controls directly in ExecNonQuery if it is not being run on the UI thread.
Nick
I have an app where users can select an Excel file, that excel file is read using an OleDbDataAdapter on another thread, and once it is finished being read it updates the CanExecute property of a Command in my ViewModel to true so the Save button is enabled.
My problem is, even though the PropertyChanged event of the command gets raised AND the CanExecute is evaluated as true, the button on the UI never gets enabled until the user does something to interact with the application (click on it, select a textbox, etc)
Here is some sample code that shows the problem. Just hook it up to two buttons bound to SaveCommand and SelectExcelFileCommand, and create an excel file with a column called ID on Sheet1 to test it.
private ICommand _saveCommand;
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
_saveCommand = new RelayCommand(Save, () => (FileContents != null && FileContents.Count > 0));
// This runs after ReadExcelFile and it evaluates as True in the debug window,
// but the Button never gets enabled until after I interact with the application!
Debug.WriteLine("SaveCommand: CanExecute = " + _saveCommand.CanExecute(null).ToString());
return _saveCommand;
}
}
private void Save() { }
private ICommand _selectExcelFileCommand;
public ICommand SelectExcelFileCommand
{
get
{
if (_selectExcelFileCommand == null)
_selectExcelFileCommand = new RelayCommand(SelectExcelFile);
return _selectExcelFileCommand;
}
}
private async void SelectExcelFile()
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.DefaultExt = ".xls|.xlsx";
dlg.Filter = "Excel documents (*.xls, *.xlsx)|*.xls;*.xlsx";
if (dlg.ShowDialog() == true)
{
await Task.Factory.StartNew(() => ReadExcelFile(dlg.FileName));
}
}
private void ReadExcelFile(string fileName)
{
try
{
using (var conn = new OleDbConnection(string.Format(#"Provider=Microsoft.Ace.OLEDB.12.0;Data Source={0};Extended Properties=Excel 8.0", fileName)))
{
OleDbDataAdapter da = new OleDbDataAdapter("SELECT DISTINCT ID FROM [Sheet1$]", conn);
var dt = new DataTable();
// Commenting out this line makes the UI update correctly,
// so I am assuming it is causing the problem
da.Fill(dt);
FileContents = new List<int>() { 1, 2, 3 };
OnPropertyChanged("SaveCommand");
}
}
catch (Exception ex)
{
MessageBox.Show("Unable to read contents:\n\n" + ex.Message, "Error");
}
}
private List<int> _fileContents = new List<int>();
public List<int> FileContents
{
get { return _fileContents; }
set
{
if (value != _fileContents)
{
_fileContents = value;
OnPropertyChanged("FileContents");
}
}
}
EDIT
I've tried using the Dispatcher to send the PropertyChanged event at a later priority, and moving the PropertyChanged call outside of the async method, but neither solution works to update the UI correctly.
It DOES work if I either remove the threading, or launch the process that reads from Excel on the dispatcher thread, but both of these solutions cause the application to freeze up while the excel file is being read. The whole point of reading on a background thread is so the user can fill out the rest of the form while the file loads. The last file this app got used for had almost 40,000 records, and made the application freeze for a minute or two.
From what I can follow this might be what you need.
public static void ExecuteWait(Action action)
{
var waitFrame = new DispatcherFrame();
// Use callback to "pop" dispatcher frame
action.BeginInvoke(dummy => waitFrame.Continue = false, null);
// this method will wait here without blocking the UI thread
Dispatcher.PushFrame(waitFrame);
}
And calling the following
if (dlg.ShowDialog() == true)
{
ExecuteWait(()=>ReadExcelFile(dlg.FileName));
OnPropertyChanged("SaveCommand");
}
not sure, but if you remove the await - does it help ?
EDIT:
I am no expert on C# 5 but what I gather that await wait for the launched task(s) to finish... it is a way to synchronize so the after the await the result be accessed without further checking whether the task(s) already finished...
From the post I think that await is not needed and that it somehow "blocks" the OnPropertyChange call from the insise the launched Task.
EDIT 2 - another try:
if (dlg.ShowDialog() == true)
{
string FN = dlg.FileName;
Task.Factory.StartNew(() => ReadExcelFile(FN));
}
EDIT 3 - solution (without C# 5 though):
I created a fresh WPF app, put 2 buttons (button1 => select excel file, button2 => Save) in the designer... I removed all "OnPropertyChanged" calls (I used this.Dispatch.Invoke instead)... RelayCommand is 1:1 from http://msdn.microsoft.com/en-us/magazine/dd419663.aspx ... following is the relevant changed source:
private void SelectExcelFile()
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.DefaultExt = ".xls|.xlsx";
dlg.Filter = "Excel documents (*.xls, *.xlsx)|*.xls;*.xlsx";
if (dlg.ShowDialog() == true)
{
Task.Factory.StartNew(() => ReadExcelFile(dlg.FileName));
}
}
private List<int> _fileContents = new List<int>();
public List<int> FileContents
{
get { return _fileContents; }
set
{
if (value != _fileContents)
{
_fileContents = value;
this.Dispatcher.Invoke ( new Action (delegate()
{
button2.IsEnabled = true;
button2.Command = SaveCommand;
}),null);
}
}
}
private void button1_Click(object sender, RoutedEventArgs e)
{
button2.IsEnabled = false;
button2.Command = null;
SelectExcelFileCommand.Execute(null);
}
private void button2_Click(object sender, RoutedEventArgs e)
{
SaveCommand.Execute(null);
}
all problems described by the OP are gone: the Excel reading is on another thread... the UI does not freeze... the Savecommand gets enabled if the Excelreading is successfull...
EDIT 4:
this.Dispatcher.Invoke(new Action(delegate()
{
CommandManager.InvalidateRequerySuggested();
}), null);
you can use this instead of the IsEnabled... causes the CanExecuteChanged event to be fired without "rebuilding" the SaveCommand (which causes the CanExecuteChanged event to be unregistered and then reregistered)
I still have no idea what it's problem is, but I have found a workaround. I simply set my SaveCommand = null and raise a PropertyChanged event to re-create the Command (the set method on the command builds the RelayCommand if it is null).
I have no idea why simply raising the PropertyChanged event won't update the UI. According to my Debug, the get method is getting called again and evaluating at CanExecute = true even though the UI doesn't update.
private async void SelectExcelFile()
{
var dlg = new Microsoft.Win32.OpenFileDialog();
dlg.DefaultExt = ".xls|.xlsx";
dlg.Filter = "Excel documents (*.xls, *.xlsx)|*.xls;*.xlsx";
if (dlg.ShowDialog() == true)
{
await Task.Factory.StartNew(() => ReadExcelFile(dlg.FileName));
}
}
private void ReadExcelFile(string fileName)
{
try
{
using (var conn = new OleDbConnection(string.Format(#"Provider=Microsoft.Ace.OLEDB.12.0;Data Source={0};Extended Properties=Excel 8.0", fileName)))
{
OleDbDataAdapter da = new OleDbDataAdapter("SELECT DISTINCT [File Number] FROM [Sheet1$]", conn);
var dt = new DataTable();
// Line that causes the problem
da.Fill(dt);
FileContents = new List<int>() { 1, 2, 3 };
// Does NOT update the UI even though CanExecute gets evaluated at True after this runs
// OnPropertyChanged("SaveCommand");
// Forces the Command to rebuild which correctly updates the UI
SaveCommand = null;
}
}
catch (Exception ex)
{
MessageBox.Show("Unable to read contents:\n\n" + ex.Message, "Error");
}
}
private ICommand _saveCommand;
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
_saveCommand = new RelayCommand(Save, () => (FileContents != null && FileContents.Count > 0));
// This runs after ReadExcelFile and it evaluates as True in the debug window!
Debug.WriteLine("SaveCommand: CanExecute = " + _saveCommand.CanExecute(null).ToString());
return _saveCommand;
}
set
{
if (_saveCommand != value)
{
_saveCommand = value;
OnPropertyChanged("SaveCommand");
}
}
}