【问题标题】:How do I prevent blocking the UI due to fast-changing properties during IO operations?如何防止在 IO 操作期间由于属性快速变化而阻塞 UI?
【发布时间】:2019-05-03 13:04:43
【问题描述】:

我正在开发一个有时会删除文件夹的应用程序。为了向用户显示进度,我在视图中使用了ProgressBar。因此,我的 ViewModel double SetupProgress 和 double SetupProgressMax 上有两个属性。我编译了要删除的文件夹中包含的所有文件的列表,每次成功删除文件后,我都会更新属性SetupProgress。事先不知道要删除多少个文件。

我的代码(浓缩到重要部分):

public class ViewModel : INotifyPropertyChanged
{
    public double SetupProgress
    {
        get; set; // Notifies about changes
    }

    public double SetupProgressMax
    {
        get; set; // Notifies about changes
    }

    public async Task<bool> DeleteFiles(IList<string> filesToBeDeleted)
    {
        bool success = true;
        SetupProgressMax = filesToBeDeleted.Count;

        foreach (string filePath in filesToBeDeleted)
        {
            success = success && await IOHelper.TryDeleteFile(filePath);

            if (success)
            {
                // Report that one item has been processed.
                _OnProgressChanged(); 
            }
            else
            {
                break;
            }
        }

        return success;
    }

    public void _OnProgressChanged()
    {
        // SetupProgress is the VM property bound by the ProgressBar
        SetupProgress++;
    }
}

public static class IOHelper
{
    public static async Task<bool> TryDeleteFile(string filePath, int tries = 3)
    {
        while (tries > 0)
        {
            try
            {
                FileInfo fi = new FileInfo(filePath);
                if (fi.IsReadOnly)
                {
                    fi.IsReadOnly = false;
                }
                fi.Delete();
                return true;
            }
            catch (FileNotFoundException)
            {
                return true;
            }
            catch (Exception ex)
            {
                tries--;
                if (tries == 0)
                {
                    // Log error
                }
                else
                {
                    // Log warning
                }
                await Task.Delay(50);
            }
        }

        return false;
    }
}

我的问题是,在删除文件时,UI线程完全阻塞,只有在操作完成后才会更新(所有文件都已删除)。

问题

  1. UI 进程阻塞的原因可能是什么?
  2. 我该如何规避这个问题?

更新:删除了我在发布问题之前测试过的解决方案,因为它们不起作用,甚至无法解决问题的根源。

【问题讨论】:

  • 你的问题是这个TryDeleteFile 因为fi.Delete(); 是一个阻塞调用并且不是异步的......看看await Task.Run 因为恕我直言,你将得到的接近。
  • 我还没有发布整个TryDeleteFile 方法。它实际上是在尝试删除给定文件的给定次数,尝试之间有 50 毫秒的延迟。我将更新问题中的代码。
  • 现在我了解到我提出的解决方案无法解决我的问题,我是否应该删除它们以使问题更短,从而更清晰?
  • 如果它们不相关会很好,它确实有帮助,谢谢!

标签: c# wpf user-interface mvvm io


【解决方案1】:

似乎TryDeleteFile 正在 UI 线程中执行。鉴于其当前实现,它不是异步方法,不应返回 Task 而是 bool:

public static bool TryDeleteFile(string filePath)
{
    try
    {
        FileInfo fi = new FileInfo(filePath);
        if (fi.IsReadOnly)
        {
            fi.IsReadOnly = false;
        }
        fi.Delete();
        return true;
    }
    catch (FileNotFoundException)
    {
        return true;
    }
    catch (Exception ex)
    {
        // Log Exception
        return false;
    }
}

await 关键字完全没有必要,因为该方法缺少任何 await 操作。

您可以使用 Task.Run 在视图模型中的后台线程上调用同步方法:

public async Task<bool> DeleteFiles(IList<string> filesToBeDeleted)
{
    ...
    foreach (string filePath in filesToBeDeleted)
    {
        success = success && await Task.Run(() => IOHelper.TryDeleteFile(filePath));
        ...
    }
    return success;
}

请注意,使用异步 API 公开真正同步的方法被认为是一种不好的做法。更多信息请参考Stephen Toub's blog post。

【讨论】:

  • 我已经更新了问题中的代码以包含我对TryDeleteFile 的实际实现。根据您的回答,我将方法中的await Task.Delay(50) 更改为Thread.Sleep(50) 并合并了您提出的更改。现在像魅力一样工作。如此流畅。
【解决方案2】:

您的 TryDeleteFile(string filePath) 方法缺少 await 运算符,这将同步运行。

您可以将同步代码包装到异步调用中,更简单的方法是使用Task.Run

编辑

没有异步执行文件删除的本机函数。但是,通过FileStream,仍然可以进行异步文件删除。

public static bool TryDeleteFile(string filePath)
{
    try
    {
        var fi = new FileInfo(filePath);
        if (fi.IsReadOnly) fi.IsReadOnly = false;

        using (new FileStream(filePath, FileMode.Truncate, FileAccess.ReadWrite, FileShare.Delete, 1,
            FileOptions.DeleteOnClose | FileOptions.Asynchronous))
        {
        }

        return true;
    }
    catch (FileNotFoundException)
    {
        return true;
    }
    catch (Exception ex)
    {
                // Log Exception
        return false;
    }
}

用法

await Task.Run(() => TryDeleteFile(filePath));

【讨论】:

  • 使用异步 API 公开真正同步的方法被认为是一种不好的做法:devblogs.microsoft.com/pfxteam/…。 TryDeleteFile 方法不应返回 Task&lt;bool&gt; 并调用 Task.Run。视图模型负责从 UI 线程“卸载”工作。
  • 我没有发布整个方法,因为我认为它不会那么重要。但显然,在 try 块中进行纯同步操作,而在 catch 块中进行唯一的异步操作,这显然不是一个好习惯……
猜你喜欢
  • 2019-09-15
  • 2015-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-18
  • 2011-05-24
  • 1970-01-01
  • 2018-09-04
相关资源
最近更新 更多