65.9K
CodeProject 正在变化。 阅读更多。
Home

异步 MVVM 现代 UI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (10投票s)

2012 年 8 月 16 日

CPOL

3分钟阅读

viewsIcon

67455

downloadIcon

3479

使用 MVVM 与 Windows 8 现代 UI。

Modern UI Async Loading

引言

目前有很多关于使用 MVVM 与 Windows 8 现代 UI 的信息和教程,也有关于使用 async 的信息。 本文的目的是展示一个完整的示例,使用 async 方法和数据模板保留设计模式,其中包括数据绑定、命令和属性。

背景

我建议你了解以下基础知识

  • MVVM
  • async
  • XAML 数据绑定
  • Lambda 表达式

这并非严格必要,但它将使学习这两种方法的结合更容易。

模型

模型非常简单,我通常更喜欢为 View 添加一个 Presenter 而不是添加 Converter,使用你想要的选项。 请注意,在 W8RP 中,日期格式已更改为 `GetDateTimeFormats`。

public class FileModel
{
  public string Name { get; set; }
  public DateTime Date { get; set; }

  public string DatePresenter
  {
      get
      {
         if(this.Date!=null)
          return this.Date.GetDateTimeFormats()[0];
         return String.Empty;
      }
  }

  public override string ToString()
  {
      return Name;
  }
}

ViewModel

ViewModel 实现了一个定制版本的 `INotifyPropertyChanged`,以便异步更新 UI。

实现INotifyPropertyChanged

要在使用 async 时更新 UI,你必须使用当前的窗口调度程序。 就像你看到的,我创建了一个更新 UI 的 async 方法,名为 '`UIThreadAction`',以及几个像 '`PropertyChangedAsync`' 这样的方法,它在调度程序操作内部调用 '`PropertyChanged`'。

#region INotifyPropertyChanged with Dispatcher
public event PropertyChangedEventHandler PropertyChanged;
private CoreDispatcher _dispatcher = null;
private async Task UIThreadAction(Action act)
{
    await _dispatcher.RunAsync(CoreDispatcherPriority.Normal,()=> act.Invoke());
}
private async void PropertyChangedAsync(string property)
{
    if (PropertyChanged != null)
        await UIThreadAction(()=> PropertyChanged(this, new PropertyChangedEventArgs(property)));
}
#endregion

属性和字段

  • 布尔值 '`IsBusy`' 和 '`IsIdle`' 以激活“读取”和“取消”按钮。
  • 集合 'Files'。 注意:作为一个 `ObservableCollection`,当您清除或添加项目时,不需要调用 `PropertyChangedAsync`。
  • FileModel 'CurrentFile' 用于设置从 Files 集合中选择的项目。 注意:`ObservableCollection` 没有 Current Item,但 Control 有,因此使用双向绑定可以为您提供当前项目。
  • 命令 '`GetFilesCommand`' 用于读取文件。
  • 命令 '`CancelCommand`' 用于停止读取文件。

注意:正如你所看到的,当我更改属性时,我调用 '`PropertyChangedAsync`',因为 UI 必须在调度程序可以更新时更新。

/// <summary>
/// Set Buttons Enable/Disable while reading
/// </summary>
public bool IsBusy
{
    get { return !_isidle; }
}
private bool _isidle;
public bool IsIdle
{
    get { return _isidle; }
    set
    {
        _isidle = value;
        PropertyChangedAsync("IsIdle");
        PropertyChangedAsync("IsBusy");
    }
}

/// <summary>
/// Set Name of the current file
/// </summary>
private FileModel _currentfile;
public FileModel CurrentFile
{
    get { return _currentfile; }
    set
    {
        _currentfile = value;
        PropertyChangedAsync("CurrentFile");
    }
}

public ObservableCollection<FileModel> Files { get; set; }
public DelegateCommand<object> GetFilesCommand { get; set; }
public DelegateCommand<object> CancelCommand { get; set; }

构造函数

在这里,我初始化命令、调度程序和集合。 注意:永远不要再次实例化集合,只需清除它即可。

‘`CancelCommand`’ 很简单,只需将 `_cancel` 设置为 true。 我不必检查其他任何东西,因为 `IsIdle` 正在绑定,所以按钮只有在我可以告诉它时才会被激活。 '`GetFilesCommand`' 已经实现了一个带有 async 前缀的操作(我通过反复试验发现了这一点,我在任何文章中都没有找到它),在其中我调用 '`await GetFilesAsync`' 来获取文件,因为应用程序可以。

public MainViewModel()
{
    _dispatcher = Window.Current.Dispatcher;

    Files = new ObservableCollection<FileModel>();
    IsIdle = true;

    CancelCommand = new DelegateCommand<object> ((ob) =>
        {
            _cancel = true;
        });


    GetFilesCommand = new DelegateCommand<object> (
        async (ob) =>
        {
            IsIdle = false;
            Files.Clear();

            await GetFilesAsync();
        });

    GetFilesCommand.Execute(null);
}

核心

我将 '`GetFilesAsync`' 分开以表明它只是在 `Task.Run` 内部调用一个方法。 该方法需要使用 async 作为前缀,以了解您希望它在“后台”运行。 '`addfile`' 是在 `_cancel` 没有被触发时添加文件的操作,如您所见,我不需要任何特殊的逻辑来知道取消的值。

最重要的是在设计时和运行时都有数据,最后一部分从调度程序在运行时调用 Invoke,通常是从设计时调用。

  • 在设计模式下,我调用了 10 次
  • 在运行时,我调用了 10K 次来查看加载项目的行为
public Task GetFilesAsync()
{
    return Task.Run(() => GetFiles());
}

Action addfile = null;
public async void GetFiles()
{
    int i = 0;
    Random rnd = new Random(DateTime.Now.Millisecond);

    addfile = () =>
        {
            if (!_cancel)
            {
                Files.Add(new FileModel()
                {
                    Date = DateTime.Now.AddDays(rnd.Next(-10, 0)),
                    Name = String.Concat("prueba", i, ".xml")
                });
            }
            else
            {
                i = 10000;
                _cancel = false;
            }
        };


    while (++i < (Windows.ApplicationModel.DesignMode.DesignModeEnabled ? 10 : 10000))
    {
        if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
            addfile.Invoke();
        else
            await UIThreadAction(() => addfile.Invoke());
    }
    IsIdle = true;
}

视图

最后,我们来到了 XAML,我创建了一个简单的页面,其中包含命令按钮、`GridView` 和用于所选项目的 `TextBlock`。 注意以下事项

  • 我在 `Page.DataContext` 中创建了 ViewModel 的一个实例
  • 第一个按钮将 Command 绑定到 '`GetFilesCommand`',并将 `IsEnabled` 绑定到 `IsIdle`
  • 第二个按钮将 Command 绑定到 '`CancelCommand`',并将 `IsEnabled` 绑定到 `IsBusy`
  • `TextBlock` 将 `Text` 绑定到 `CurrentFile`
  • `GridView` 将 `ItemsSource` 绑定到 'Files',并将 `SelectedItem` 绑定到 `CurrentFile` 双向模式
<Page
    x:Class="AsyncMVVM.MainPage"
    IsTabStop="false"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AsyncMVVM"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="using:AsyncMVVM.ViewModel"
    mc:Ignorable="d">
    
    <Page.DataContext>
        <vm:MainViewModel/>
    </Page.DataContext>

    <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="7*"/>
            <RowDefinition Height="41*"/>
        </Grid.RowDefinitions>
        <Button Content="Read Files" HorizontalAlignment="Left" Height="56" 
           Margin="61,46,0,0" VerticalAlignment="Top" Width="118" 
           Command="{Binding GetFilesCommand}" IsEnabled="{Binding IsIdle}"/>
        <Button Content="Cancel" HorizontalAlignment="Left" Height="56" 
           Margin="184,46,0,0" VerticalAlignment="Top" Width="118" 
           Command="{Binding CancelCommand}" IsEnabled="{Binding IsBusy}"/>
        <TextBlock HorizontalAlignment="Left" Height="34" Margin="488,68,0,0" 
          TextWrapping="Wrap"  VerticalAlignment="Top" Width="403" Text="{Binding CurrentFile}"/>
        <GridView  HorizontalAlignment="Left" Height="580" Margin="61,14,0,0" 
                   VerticalAlignment="Top" Width="1275" Grid.Row="1" ItemsSource="{Binding Files}" 
                   SelectedItem="{Binding CurrentFile, Mode=TwoWay}">
            <GridView.ItemTemplate>
                <DataTemplate>
                <Border Background="LimeGreen" Width="140"  BorderBrush="Lime" BorderThickness="3" >
                    <Grid >
                    <TextBlock Text="{Binding Name}"></TextBlock>
                    <TextBlock Margin="0,50,0,0" Text="{Binding DatePresenter}"></TextBlock>
                    </Grid>
                </Border>
            </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Page>

使用代码

要成功编译它,您需要委托命令代码

public class DelegateCommand<T> : ICommand
{
    readonly Action<T> callback;

    public DelegateCommand(Action<T> callback)
    {
        this.callback = callback;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        if (callback != null) { callback((T)parameter); }
    }
}

关注点

我鼓励您运行演示,因为我认为最好是查看它,以了解实际发生了什么。

历史

v 1.0 原始版本。

© . All rights reserved.