Использование OleDbDataAdapter в фоновом потоке приводит к тому, что мой пользовательский интерфейс не обновляется правильно?
У меня есть приложение, где пользователи могут выбрать файл Excel, этот файл Excel читается с использованием OleDbDataAdapter
в другом потоке, и после завершения чтения он обновляет свойство CanExecute команды в моем ViewModel до true, поэтому кнопка "Сохранить" включена.
Моя проблема в том, что, хотя событие PropertyChanged команды возникает и CanExecute оценивается как истина, кнопка на пользовательском интерфейсе никогда не включается, пока пользователь не сделает что-то для взаимодействия с приложением (щелкните по нему, выберите текстовое поле и т. Д.).)
Вот пример кода, который показывает проблему. Просто подключите его к двум кнопкам, привязанным к SaveCommand
а также SelectExcelFileCommand
и создайте файл excel со столбцом с именем ID
на Sheet1
чтобы проверить это.
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");
}
}
}
РЕДАКТИРОВАТЬ
Я попытался с помощью Dispatcher отправить событие PropertyChanged с более поздним приоритетом и переместить вызов PropertyChanged за пределы асинхронного метода, но ни одно из решений не работает для корректного обновления пользовательского интерфейса.
Это работает, если я удаляю поток или запускаю процесс, который читает из Excel в потоке диспетчера, но оба эти решения приводят к зависанию приложения во время чтения файла Excel. Весь смысл чтения в фоновом потоке заключается в том, что пользователь может заполнить оставшуюся часть формы во время загрузки файла. В последнем файле, для которого использовалось это приложение, было почти 40000 записей, и приложение зависало на минуту или две.
3 ответа
Из того, что я могу понять, это может быть то, что вам нужно.
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);
}
И называя следующее
if (dlg.ShowDialog() == true)
{
ExecuteWait(()=>ReadExcelFile(dlg.FileName));
OnPropertyChanged("SaveCommand");
}
Не уверен, но если вы удалите await
- это помогает?
РЕДАКТИРОВАТЬ:
Я не эксперт по C# 5, но что я понял await
дождитесь завершения запущенной задачи, это способ синхронизации, поэтому после await
доступ к результату без дальнейшей проверки, завершено ли задание (я)... Из поста я думаю, что await
не нужно и что он как-то "блокирует" OnPropertyChange
вызов из инсайса запущенной задачи.
РЕДАКТИРОВАТЬ 2 - еще одна попытка:
if (dlg.ShowDialog() == true)
{
string FN = dlg.FileName;
Task.Factory.StartNew(() => ReadExcelFile(FN));
}
РЕДАКТИРОВАТЬ 3 - решение (но без C# 5):
Я создал свежее приложение WPF, поставил 2 кнопки (button1
=> выбрать файл Excel, button2
=> Сохранить) в конструкторе... Я все убрал "OnPropertyChanged
"звонки (я использовал this.Dispatch.Invoke
вместо)... RelayCommand
1:1 от http://msdn.microsoft.com/en-us/magazine/dd419663.aspx... ниже приведен соответствующий измененный источник:
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);
}
все проблемы, описанные в OP, исчезли: чтение Excel происходит в другом потоке... пользовательский интерфейс не зависает... Savecommand
включается, если Excel reading успешно...
РЕДАКТИРОВАТЬ 4:
this.Dispatcher.Invoke(new Action(delegate()
{
CommandManager.InvalidateRequerySuggested();
}), null);
Вы можете использовать это вместо IsEnabled
... вызывает CanExecuteChanged
событие, которое будет запущено без "восстановления" SaveCommand
(что вызывает CanExecuteChanged
событие должно быть незарегистрировано, а затем перерегистрировано)
Я до сих пор не понимаю, в чем проблема, но я нашел обходной путь. Я просто установил свой SaveCommand = null
и поднять PropertyChanged
событие для воссоздания Команды (set
метод по команде создает RelayCommand, если он имеет значение null).
Я понятия не имею, почему простое повышение события PropertyChanged не обновит интерфейс. Согласно моему отладке, get
метод вызывается снова и оценивается в CanExecute = true
даже если пользовательский интерфейс не обновляется.
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");
}
}
}