异步数据加载与绑定设计: AsyncLoadHelper

在现代应用程序开发中,异步数据加载和管理是一个普遍的需求。本文介绍了一个自定义的异步数据加载工具——AsyncLoadHelper。通过详细的设计和实现介绍,以及结合实际应用示例,展示了AsyncLoadHelper在简化异步操作、提高代码可读性和维护性方面的优势。

在开发过程中,异步数据加载常常涉及复杂的状态管理和异常处理。为了解决这些问题,提高开发效率,我开发了AsyncLoadHelper类。这个工具旨在提供一种通用且易于使用的异步数据加载和状态管理解决方案。

设计理念

AsyncLoadHelper的设计目标是实现以下功能:

  • 泛型支持:适用于多种类型的数据加载需求。
  • 状态管理:自动管理加载状态和异常信息。
  • 线程安全:使用异步锁确保同一时间只有一个加载任务在进行。
  • 取消支持:支持取消操作,防止资源浪费。

类定义和实现

下面是AsyncLoadHelper类的实现,包括支持带参数的异步数据加载版本。

AsyncLoadHelper<TData>

这个类适用于不需要额外参数的数据加载场景。

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;

namespace AutoMold.Shared.Base
{
    public class AsyncLoadHelper<TData> : BindableBase, IDisposable
  {
      private TData _data;
      private bool _isLoading;
      private Exception _loadingException;
      private readonly Lazy<DelegateCommand> _loadCommand;
      private readonly Func<CancellationToken, Task<TData>> _dataLoadMethod;
      private CancellationTokenSource _cts;
      private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);

      public TData Data
      {
          get => _data;
          set => SetProperty(ref _data, value);
      }

      public bool IsLoading
      {
          get => _isLoading;
          set => SetProperty(ref _isLoading, value);
      }

      public Exception LoadingException
      {
          get => _loadingException;
          set => SetProperty(ref _loadingException, value);
      }

      public DelegateCommand LoadCommand => _loadCommand.Value;

      public AsyncLoadHelper(Func<CancellationToken, Task<TData>> dataLoadMethod)
      {
          _dataLoadMethod = dataLoadMethod;
          _loadCommand = new Lazy<DelegateCommand>(() =>
              new DelegateCommand(async () => await ExecuteLoadDataAsync(), () => !IsLoading).ObservesProperty(() => IsLoading));
      }

      public virtual async Task ExecuteLoadDataAsync()
      {
          // 如果当前已经在加载数据,则直接返回,防止重复加载
          if (IsLoading) return;
          // 等待异步锁,确保同时只有一个加载任务在进行
          await _asyncLock.WaitAsync();

          // 取消当前的加载任务(如果有),并创建一个新的CancellationTokenSource
          _cts?.Cancel();
          _cts = new CancellationTokenSource();

          // 将IsLoading属性设置为true,表示当前正在加载数据
          IsLoading = true;
          // 重置LoadingException属性为null,清除之前的异常信息
          LoadingException = null;

          try
          {
              // 异步调用数据加载方法,并将结果赋值给Data属性
              Data = await _dataLoadMethod(_cts.Token);
          }
          catch (OperationCanceledException)
          {
              // 捕获操作取消异常,可以根据需要处理
              // 这里没有特殊处理,因为取消操作是预期的行为
              // Handle if needed
          }
          catch (Exception e)
          {
              // 捕获所有其他异常,并将异常赋值给LoadingException属性,以便在UI中显示或记录
              LoadingException = e;
          }
          finally
          {
              // 无论是否发生异常,都将IsLoading属性设置为false,表示加载任务已完成
              IsLoading = false;
              // 释放异步锁,允许其他加载任务执行
              _asyncLock.Release();
          }
      }

      public void Dispose()
      {
          _cts?.Cancel();
          _cts?.Dispose();
          _asyncLock.Dispose();
      }
  }
}

AsyncLoadHelper<TData, TParameter>

这个类适用于需要额外参数的数据加载场景。

using Prism.Commands;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;

namespace AutoMold.Shared.Base
{
    public class AsyncLoadHelper<TData, TParameter> : BindableBase, IDisposable
    {
        private TData _data;
        private bool _isLoading;
        private Exception _loadingException;
        private readonly Lazy<DelegateCommand<TParameter>> _loadCommand;
        private readonly Func<TParameter, CancellationToken, Task<TData>> _dataLoadMethod;
        private CancellationTokenSource _cts;
        private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);

        public TData Data
        {
            get => _data;
            set => SetProperty(ref _data, value);
        }

        public bool IsLoading
        {
            get => _isLoading;
            set => SetProperty(ref _isLoading, value);
        }

        public Exception LoadingException
        {
            get => _loadingException;
            set => SetProperty(ref _loadingException, value);
        }

        public DelegateCommand<TParameter> LoadCommand => _loadCommand.Value;

        public AsyncLoadHelper(Func<TParameter, CancellationToken, Task<TData>> dataLoadMethod)
        {
            _dataLoadMethod = dataLoadMethod;
            _loadCommand = new Lazy<DelegateCommand<TParameter>>(() =>
                new DelegateCommand<TParameter>(async param => await ExecuteLoadDataAsync(param), _ => !IsLoading).ObservesProperty(() => IsLoading));
        }

        public virtual async Task ExecuteLoadDataAsync(TParameter parameter)
        {
            if (IsLoading) return;
            await _asyncLock.WaitAsync();

            _cts?.Cancel();
            _cts = new CancellationTokenSource();
            IsLoading = true;
            LoadingException = null;

            try
            {
                Data = await _dataLoadMethod(parameter, _cts.Token);
            }
            catch (OperationCanceledException)
            {
                // Handle if needed
            }
            catch (Exception e)
            {
                HandleException(e);
            }
            finally
            {
                IsLoading = false;
                _asyncLock.Release();
            }
        }

        protected virtual void HandleException(Exception exception)
        {
            // Handle the exception here, e.g., log it or show to user
            LoadingException = exception;
        }

        public void Dispose()
        {
            _cts?.Cancel();
            _cts?.Dispose();
            _asyncLock.Dispose();
        }
    }
}

应用实例

以下代码展示了如何在实际项目中使用AsyncLoadHelper类进行异步数据加载:

ViewModel部分

/// <summary>
/// 速度参数
/// </summary>
public AsyncLoadHelper<ObservableCollection<DIntVal>> EditableValsLoader { get; }
/// <summary>
/// 实时信息
/// </summary>
public AsyncLoadHelper<ObservableCollection<DIntVal>> ViewableValsLoader { get; }

public HomePageViewModel(ILogger<HomePageViewModel> logger, IDIntValGroupRepository dintValGroupRepo, 
    IPLCService plc)
{
    _logger = logger;
    _dintValGroupRepo = dintValGroupRepo;
    _plc = plc;

    _timer = new Timer();
    _timer.Interval = 150;
    _timer.Elapsed += SyncViewableFromPLC;

    _timerLazy = new Timer();
    _timerLazy.Interval = 1000;
    _timerLazy.Elapsed += SyncEditableToPLC;

    EditableValsLoader = new AsyncLoadHelper<ObservableCollection<DIntVal>>(async cts =>
    {
        var list = await _dintValGroupRepo.GetEditablesAsync();
        return new ObservableCollection<DIntVal>(list);
    });
    EditableValsLoader.LoadCommand.Execute();
    
    ViewableValsLoader = new AsyncLoadHelper<ObservableCollection<DIntVal>>(async cts =>
    {
        var list = await _dintValGroupRepo.GetViewablesAsync();
        return new ObservableCollection<DIntVal>(list);
    });
    ViewableValsLoader.LoadCommand.Execute();
}

XAML部分

<Grid Grid.Column="0">
    <GroupBox
        BorderBrush="{DynamicResource BorderColorDefault}"
        Header="实时信息"
        Style="{x:Null}">
        <ItemsControl
            Margin="5"
            d:ItemsSource="{x:Static vm:HomePageViewModel.DesignViewableVals}"
            FontSize="20"
            FontWeight="Bold"
            ItemsSource="{Binding ViewableValsLoader.Data}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid Columns="1" />
                </ItemsControl.ItemsPanel>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="120" />
                            <ColumnDefinition Width="120" />
                        </Grid.ColumnDefinitions>
                        <TextBlock
                            Grid.Column="0"
                            Margin="5"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Text="{Binding DisplayName}" />
                        <TextBlock
                            Grid.Column="1"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Style="{x:Null}"
                            Text="{Binding Val, StringFormat={}{0:F1}, Converter={StaticResource IntDivide10ToFloatConverter}}" />
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </GroupBox>
    <hc:LoadingCircle
        Margin="6"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Foreground="{DynamicResource ForegroundDefault}"
        Visibility="{Binding ViewableValsLoader.IsLoading, Converter={StaticResource Boolean2VisibilityConverter}}" />
</Grid>

AsyncLoadHelper类通过提供一种通用、易于使用的异步数据加载和状态管理方案,极大地简化了异步编程中的复杂性。无论是无需额外参数的简单数据加载,还是需要参数的复杂场景,AsyncLoadHelper都能轻松应对。希望本文能帮助开发者更好地理解和应用AsyncLoadHelper,从而提高开发效率和代码质量。

posted @ 2024-05-28 14:29  非法关键字  阅读(7)  评论(0编辑  收藏  举报