异步 MVVM 现代 UI






4.93/5 (10投票s)
使用 MVVM 与 Windows 8 现代 UI。
引言
目前有很多关于使用 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 原始版本。