Использование OleDbDataAdapter в фоновом потоке приводит к тому, что мой пользовательский интерфейс не обновляется правильно? - PullRequest
1 голос
/ 22 июля 2011

У меня есть приложение, в котором пользователи могут выбрать файл 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.Весь смысл чтения в фоновом потоке заключается в том, чтобы пользователь мог заполнить оставшуюся часть формы во время загрузки файла.В последнем файле, для которого использовалось это приложение, было почти 40 000 записей, и приложение зависало на одну или две минуты.

Ответы [ 3 ]

1 голос
/ 09 августа 2011

Из того, что я могу понять, это может быть то, что вам нужно.

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");
    }     
1 голос
/ 22 июля 2011

не уверен, но если вы удалите await - это поможет?

РЕДАКТИРОВАТЬ:

Я не эксперт по C # 5, но что я понимаю, что await ждатьзапущенные задачи завершить ... это способ синхронизации, поэтому после await результата можно получить доступ без дальнейшей проверки, завершена ли задача (задачи) ... Из поста я думаю, что await не требуется, и что он каким-то образом «блокирует» вызов OnPropertyChange от запуска запущенной Задачи.

EDIT 2 - еще одна попытка:

if (dlg.ShowDialog() == true)
    {
        string FN = dlg.FileName;
        Task.Factory.StartNew(() => ReadExcelFile(FN));
    }

EDIT 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 включается, если Excelreading успешно завершенl ...

РЕДАКТИРОВАТЬ 4:

                this.Dispatcher.Invoke(new Action(delegate()
                {
                    CommandManager.InvalidateRequerySuggested();
                }), null);

вы можете использовать это вместо IsEnabled ... вызывает событие CanExecuteChanged, которое происходит без "перестройки" SaveCommand (что приводит к тому, что событие CanExecuteChanged не регистрируется, а затем перерегистрируется)

0 голосов
/ 23 июля 2011

Я до сих пор не знаю, в чем проблема, но я нашел обходной путь. Я просто устанавливаю свой SaveCommand = null и вызываю событие PropertyChanged, чтобы заново создать Команду (метод set в команде создает RelayCommand, если он нулевой).

Я понятия не имею, почему простое поднятие события PropertyChanged не обновит интерфейс. Согласно моему Debug, метод 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");
        }
    }
}
...