1

I'm creating an desktop application with UWP and C#. Basically, I want to read and elaborate an Excel file. During the elaboration of this file, I want to display in the UI a progress bar and a text message. For that, I update the property in the view model: for example for the message the code is:

private string _message;

public string Message
{
    get { return _message; }
    set
    {
        if (_message != value)
        {
            _message = value;
            OnPropertyChanged(nameof(Message));
        }
    }
}

So, in the view model I have the function to read and elaborate the file.

public async Task DecodeFile()
{
    ImportExcel excel = new ImportExcel();
    excel.ReadCompleted += Excel_ReadCompleted;
    excel.ReadHeader += Excel_ReadHeader;
    excel.UpdatedRow += Excel_UpdatedRow;

    await excel.ReadToGrid(FileName);
}

If I use the function as it is, the UI is freezing when it executes this code. Then, I tried to change the last line with

await System.Threading.Tasks.Task.Run(() => excel.ReadToGrid(FileName));

but in this case I have another kind of error.

enter image description here

What is the best/correct practice to resolve this issue?

Update

The function ReadToGrid is part of a class called ImportExcel and it reads the Excel file with SpreadsheetDocument using the package DocumentFormat.OpenXml.Spreadsheet. In this class there are few events defined like

public event EventHandler<UpdatedRowEventArgs> UpdatedRow;
protected virtual void OnUpdatedRow(UpdatedRowEventArgs e)
{
    EventHandler<UpdatedRowEventArgs> handler = UpdatedRow;
    if (handler != null) handler(this, e);
}

I create an instance of this class in the view model. When an event is raised, the view model updates a property. For example, when it reads a new row of the Excel file, UpdateRow is raised and the view model updates the property CurrentRow related to the Value in the ProgressBar

private void Excel_UpdatedRow(object sender, UpdatedRowEventArgs e)
{
    CurrentRow = e.CurrentRow;
    Message = $"Read record {e.CurrentRow}/{e.TotalRows}";
}

In the same event, I change the property Message. None of them are changing in the UI. The UI is completely freezed.

Update 2

I tried to change the OnPropertyChanged but the UI is still freezed

public event PropertyChangedEventHandler PropertyChanged;

private async void OnPropertyChanged(string propertyName)
{
    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
    await Window.Current.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
        {
            PropertyChanged?.Invoke(this, 
                 new PropertyChangedEventArgs(propertyName));
        });
}

Update 4

I have published the full source code on GitHub.

Update 5 - Fix the issue

I have fixed the problem. I removed the call to ReadToGrid from the UI and moved in the view model. I removed all the code behind and replaced it with properties in the view model. Added everywhere

CoreDispatcher dispatcher = CoreApplication.MainView?.CoreWindow?.Dispatcher;
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
});

and I changed OnPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

protected async void OnPropertyChanged(string propertyName)
{
    PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);

    await dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
    {
        PropertyChanged?.Invoke(this,
                new PropertyChangedEventArgs(propertyName));
    });
}

For future reference or if someone else has the same issue, the full source code is on GitHub.

Enrico
  • 3,592
  • 6
  • 45
  • 102
  • Try to put DecodeFile in Task - Task.Run(() => {your decodeFile code}); PS: please, use binding - it will help in the future people who will be working on this project and it helps avoid such problems also – Sasha May 11 '21 at 07:54
  • What do you mean? I'm using binding – Enrico May 11 '21 at 08:32
  • What exactly does `ReadToGrid` do? How is it implemented? – mm8 May 11 '21 at 14:29
  • It is reading an Excel file with `DocumentFormat.OpenXml.Spreadsheet` and return an object with the content of the file. – Enrico May 11 '21 at 14:34
  • It's hard to detect your problem, it's helpful to share [mcve]. – Nico Zhu May 12 '21 at 08:43

3 Answers3

2

UWP UI freezes when reads a file

The problem is you read the file in the UI thread that freezes the UI, for solve this problem you could use task to call ReadToGrid method, then go back to UI thread when Excel_ReadCompleted event like the following.

public async Task DecodeFile()
{

    await Task.Run(async () =>
    {
        ImportExcel excel = new ImportExcel();
        excel.ReadCompleted += Excel_ReadCompleted;
        excel.ReadHeader += Excel_ReadHeader;
        excel.UpdatedRow += Excel_UpdatedRow;
        await excel.ReadToGrid(FileName);

    });
}

private async void Excel_ReadCompleted(object sender, Excel.CustomEventArgs.ReadCompletedEventArgs e)
{
    var dispatcher = CoreApplication.MainView?.CoreWindow?.Dispatcher;
    grid = e.DataGrid;
    await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
     {
         for (int i = 0; i < e.DataGrid.Headers.Count; i++)
         {
             ListItemLeft.Add(new ListItemData() { Index = i, ListItemText = e.DataGrid.Headers[i] });
         }
     });

    OnPropertyChanged(nameof(ListItemLeft));
    MoveRightCommand.RaiseCanExecuteChanged();
    MoveLeftCommand.RaiseCanExecuteChanged();
}
Nico Zhu
  • 32,367
  • 2
  • 15
  • 36
  • Thank you for your answer. I have changed the code in particular `Excel_UpdatedRow` where I update the properties but the UI doesn't change. Also, when the procedure `ReadToGrid` is completed, I have an error in the `SplitPage.xaml.cs` function `btnNext_Click` that it is where I launch the `DecodeFile` (same as the image above https://i.stack.imgur.com/V6gGA.png) – Enrico May 13 '21 at 09:28
  • you need to add dispatcher in `Excel_ReadCompleted` event handler but not `Excel_UpdatedRow`. And call a task in `DecodeFile` method. – Nico Zhu May 13 '21 at 09:34
  • And your screenshot means you update ui the no-uithread, so, we need go back to uithread manually in Excel_ReadCompleted method `ListItemLeft.Add` line – Nico Zhu May 13 '21 at 09:36
  • In `Excel_UpdateRow` I change the properties to update the UI. For example, I want to see the progression in the `ProgressBar` and a nice message. Then, at the end of the reading in the `Excel_ReadCompleted`. – Enrico May 13 '21 at 09:45
  • The `ListItemLeft.Add` is working and it displays the correct list in the UI (when it is working) – Enrico May 13 '21 at 09:48
  • if you need update ui in Excel_UpdateRow , please call dispatcher in it like Excel_ReadCompleted . – Nico Zhu May 13 '21 at 09:52
  • I did it but it doesn't work. You can see the updated code in GitHub https://github.com/erossini/UWPFileSplit/blob/main/FileSplit/ViewModels/SplitPageViewModel.cs – Enrico May 13 '21 at 09:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232341/discussion-between-nico-zhu-msft-and-enrico). – Nico Zhu May 13 '21 at 12:02
0

First, check what part is actually blocking the UI thread. Just because something returns a task does not guarantee that it is actually non-blocking. While ReadToGrid is a likely candidate, it might also be one of the events you attach.

Secondly, the exception is most probably raised because some COM object is accessed from another thread than it was created on. This is often a problem where the UI is updated from a non-UI thread, but in this case I would guess it is due to some excel COM object.

You might want to read a bit about how COM manages threads. You should also refer to the documentation of the library you are using, especially the threading rules.

JonasH
  • 28,608
  • 2
  • 10
  • 23
0

The callback methods (Excel_UpdatedRow) executes on the worker thread. Before it changes the Message property it should switch to the UI thread.

Switching to the UI thread is done using Dispatcher.BeginInvoke.

Extra: If there are a lot of asynchronous callbacks, you could change the ViewModel (or even the ViewModel's base class) to switch to the UI thread if needed.

private void UpdateUI()
{
    if (!Dispatcher.CheckAccess())
    {
        // We're not in the UI thread, use the dispatcher to call this same method on the UI thread
        Dispatcher.BeginInvoke(new Action(UpdateUI));
        return;
    }

    // We're in the UI thread, update the UI
    Meesage = ...
}
Emond
  • 50,210
  • 11
  • 84
  • 115
  • If I use this code in the view model, `Dispatcher` is not recognized. What namespace I have to use? If I use this code in the `xaml.cs`, I have this error `Core.Dispatcher does not contain a definition for CheckAccess` – Enrico May 11 '21 at 16:29
  • You would have to pass the Dispather into the viewmodel upon its construction. Only do this if you really need this in most/all of the ViewModels. See: https://stackoverflow.com/questions/2354438/how-to-pass-the-ui-dispatcher-to-the-viewmodel – Emond May 11 '21 at 20:49