Search code examples
c#wpfc#-5.0oledbdataadaptercanexecute

Using an OleDbDataAdapter on a background thread causes my UI to not update correctly?


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.


Solution

  • 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");
        }