MvvmCrudGv - (用 14 步左右构建您自己的 Wpf Mvvm 框架 CRUD 应用程序 - 实操)
又一个基本的.Net Wpf Mvvm 框架 CRUD 应用 - 针对 WPF/MVVM 初学者/练习者,通过14个(左右)简单步骤手把手教学。
引言
此应用程序的源代码也可在 GitHub 上找到:-
此应用程序中的服务与 WCF 兼容。以下是此项目的自托管 WCF 版本的链接(此版本在线程中自托管 TodoService 并在应用程序中消费它。其余功能都相同。):-
- https://github.com/amitthk/mvvmwcfcrudlist (WCF 版本)
- 下载 MvvmWcfCrudList (WCF 版本 - 源代码)
- 下载 MvvmWcfCrudListExe.zip (WCF 版本 - 仅可执行文件)
MvvmCrudGv 是一个使用 .Net wpf MVVM 模式编写的基本 CRUD 应用程序(待办事项列表应用程序)。下面是应用程序的外观:-
我们这里是第一个 CRUD 页面,显示待办事项列表,我们可以对项目执行基本的 CRUD 编辑操作。
- “新建”按钮将新项目添加到列表中,我们可以编辑并保存它。
- 我们可以单击以选择列表视图中的项目,然后进行编辑/保存/删除。
- 我们可以使用“详情”按钮进入详情页面,对所选项目进行编辑。
- 我们也可以双击列表中的项目进入详情页面。
此应用程序基本上使用基于 Protobuf 的非常简单的 CRUD 数据库实现。这里我们仅将其用于持久化,有关 Protobuf 序列化的更多详细信息,我们可以访问 [此链接]。
背景
有许多 WPF MVVM 框架可用。但是,对于初学者来说,每个框架中的功能数量可能会让人不知所措。因此,以下基本的 14 步框架应该能够给我们一个良好的开端。然后我们可以继续我们的旅程,探索更精细的框架。
我们如何创建应用程序
以下是我们创建此项目所执行的步骤。这些步骤的代码清单如下所示。关于代码的一些解释列在代码旁边,我认为这样在查看代码本身时更容易理解:-
- 在 Visual Studio 中创建一个 wpf 项目,我们称之为 MvvmCrudGv。(在线有许多快速入门教程)。(清单 1a)
- 向此解决方案添加另一个项目类型 ClassLibrary,命名为 MvvmCrudGv.Service。向服务添加引用 System.ServiceModel、System.Runtime.Serialization。(我们将在其中添加持久化服务。我们可以从此处开始编写自己的数据层来持久化服务对象。)
- 创建这些文件夹 -
- 我们添加了文件夹 ViewModels、Views、Common、Common>Behaviors、Common>Messaging(Viewmodels 存储我们的视图模型,common 存储公共文件,View 存储视图)
- 我们在 MvvmCrudGv.Service 中添加了文件夹 Entity、Persistence (清单 2a)
- 将我们简单的 Todo(我们想要持久化的基本 DataContract)添加到 MvvmCrudGv.Service.Entity 中。( 清单 2b )
- 在 MainWindow.xaml.cs 中,我们创建了一个这些 Todo 的模拟列表,并在 MainWindow.xaml 中绑定到此模拟列表以进行测试。( 清单 2c )
- 测试 MainWindow 是否绑定到 DataContext (TodoList)。(清单 2d)
- 将 MainWindow 移动到 Views 文件夹。在 App.xaml 中将启动 URI 指向正确的位置(StartupUri=Views/MainWindow.xaml)。测试是否正常工作。
- 添加一个类 ViewModels\MainWindowViewmodel。我们希望 MainWindow.xaml 使用此类作为其 ViewModel/DataContext。( 清单 4a )
- 添加 Common\ViewModelLocator。这将是定义我们的 View=>ViewModel 路由的公共类。 ( 清单 5a )
- 这是我们的 Views=>ViewModels 的注入器/映射器。
- 公开公共属性类型 MainWindowViewModel。( 清单 5b )
- 我们将把它放在“App.xaml”资源字典中的一个公共位置,以便任何人都可以随时使用这个类。
- 在 App.xaml 中添加 ResourceDictionary (清单 5c)
- 将 Views/MainWindow.xaml 绑定到 ViewModels\MainWindowViewModel (清单 5d)
- 在 Views/MainWindow.xaml 中绑定属性 DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"
- 我们将看到列表的显示将消失,因为 DataContext 已更改,我们现在应该将绑定移动到其 ViewModel。( 清单 5e )
- 从 MainWindow.xaml.cs 中移除 TodoList 逻辑,并将其移动到 MainWindowViewModel.cs。( 清单 5f ) 设置 ListView ItemsSource="{Binding TodoList}"。测试视图现在是否正确显示列表。
里程碑 1:基本结构设置完成。
(目标 2: 设置应用程序对象之间的通信平台)
- 重构以包含页面之间的导航。这可能是我们应用程序最重要的功能。
- 我们的 EventAggregator 或消息系统已经准备就绪。现在是时候使用它了。
- 创建新的 Views Home.xaml、TodoDetails.xaml 和 HomeViewModel、TodoViewModel。TodoDetails 视图我们稍后使用。首先,我们使用 Home 视图。我们的 MainWindow 一旦加载就会直接导航到 Home 视图。让我们创建框架并将导航代码连接到我们的 MainWindow。( 清单 7c )
- 将视图代码从 MainWindow.xaml 移动到 Home.xaml。将代码从 MainWindowViewModel 移动到 HomeViewModel。( 清单 7d )
- 我们的 MainWindow.xaml 将在顶部有一个菜单条,下方有一个名为 MainFrame 的框架。让我们添加代码。( 清单 7e )
- 在 ViewModelLocator 中为 HomeViewModel 和 TodoViewModel 创建属性。将 Home.xaml 和 TodoDetails.xaml 绑定到它们的 ViewModel。( 清单 7f ) (清单 7g )
- 在 App.xaml.cs 中添加一个静态 IEventAggregator。在应用程序启动时实例化此 eventAggregator。( 清单 7h )
- 在 MainWindow.xaml.cs 中,使用 App.eventAggregator 订阅 NavMessage 类型的事件,并根据 NavMessage 在 MainFrame 上执行导航。( 清单 7i )
- 我们的框架现在实际上已为 CRUD 操作准备就绪。现在让我们编写实际的服务和持久化逻辑。向项目 MvvmCrudGv.Service 添加 ServiceContract 及其实现。(ITodoService 和 TodoService 分别)(清单 8a)
- 添加一个单例 Common\BootStrapper。向其添加启动和退出引导程序例程。在 App.xaml.cs 中启用 BootStrapper(OnStartup 和 OnExit)(清单 9a)
- 现在我们需要一些辅助类来创建我们的 CRUD 操作。我们现在要做的就是非常标准的 MVVM CRUD 操作。
- - 首先,让我们添加我们的 Common\BaseViewModel 抽象类。这个类实现了 INotifyPropertyChanged,WPF/Xaml 使用它来动态绑定属性更改到视图。这个类还有一个 IsDirty 标志,一旦有任何修改,它就会被标记为 true。( 清单 10a )
- 添加 \Common\RelayCommand。这个类的作用正如其名称所示——它将命令中继到实例中定义的相应 Delegate。关于这个类,网上有很多资料。( 清单 10b )
- WPF/Xaml 的一个便利之处是转换器。有时 Xaml 需要特定类型(例如 Visibility、Color),我们希望将这些类型绑定到我们不同类型的属性(例如布尔标志 IsVisible)。现在,要将一种类型转换为另一种类型(Xaml 所需的类型),我们实现转换器进行转换并向 Xaml 提供它真正需要的东西。我们将在这里添加一些转换器。我们可以注意到这些转换器正在从一种类型转换为另一种类型。( 清单 10c )
- 在 \Styles.xaml 中添加通用样式,并将其引用添加到 App.xaml ()。在此 ResourceDictionary 中添加常用转换器和样式。( 清单 10d )
- 接下来我们要做的是更新我们的 TodoViewModel。这是我们的 MVVM 应用程序交互的基本 ViewModel 类。
- 由于 MVVM 将需要一些与呈现相关的属性和命令,并且我们不想将这些呈现属性添加到我们的服务实体中,因此我们将使用 ViewModel 包装器包装我们的服务/域实体。我们的 TodoViewModel 基本上是 Service.Entity.Todo 的一个包装器。我们的 MVVM 将与 TodoViewModel 交互,而 TodoViewModel 又会修改所包含的 Service.Entity.Todo 的一些属性。当我们想要与我们的服务通信时,我们只需移除包装器(TodoViewModel 属性)并将所包含的 Service.Entity.Todo 发送给服务。——现在让我们添加我们的 TodoViewModel 包装器。这个类继承自 BaseViewModel。TodoViewModel 也包含一些 MVVM 属性。( 清单 11a )
- 现在我们添加 CRUD 操作
- 现在我们将在 Home.xaml 中创建带有基本 CRUD 操作显示。请注意,按钮不起作用,因为命令尚未绑定到实际的委托。( 清单 12a )
- 在 HomeViewModel 中添加 CRUD 操作。( 清单 12b )
- 向 Styles.xaml 添加默认和自定义 ErrorTemplate (textBoxErrorTemplate),并将此模板用于 Textboxes 的验证。
- 添加一个简单的 NumericValidationRule 并更新目标 TextBoxes 的绑定。
- 我们还添加了简单的事件到命令 (行为) 双击,并将其挂钩到 ListView 的项目双击,以将我们带到详细信息页面。
- 接下来,我们将创建 TodoDetails.xaml,并且详情页面应该从提供的 TodoViewModel 获取其 DataContext。请注意这里有一个技巧,我们如何从导航中收集 TodoViewModel,并在我们的“TodoDetails.xaml.cs”的构造函数中覆盖它。( 清单 13a )
- 这里有一些关于导航需要注意的基本点。在视图加载时,我们用提供的 ViewModel 覆盖 DataContext,而不是默认的。请注意 TodoDetails 参数化构造函数 TodoDetails.xaml.cs 中的更改。( 清单 13b )
- 请注意 TodoViewModel 中绑定到 TodoDetails 视图的“返回”和“保存”按钮的功能。
里程碑 4:添加 MVVM CRUD 操作。
(目标 5: 添加持久层来保存我们的数据。)
- 添加持久层。( 清单 14a ) (清单 14b ) (清单 14c )
- 我们右键单击 MvvmCrudGv.Service 项目,“管理 Nuget 包...”,并在此处安装了“protobuf-net”包。
- 我们添加了 TodoPersistence 类以及关联的 ProtobufDB 和辅助类。同时更新我们的 DataContract “Todo”,添加 [ProtoContract]、[Serializable] 及其属性添加 [ProtoMember(<int>)] 属性。
- 更新 ITodoService 以使用 TodoPersistence 而不是简单的内存列表。
里程碑 5:持久层已添加。
至此,我们的 MVVM 应用程序就完成了。下面是所有代码清单。代码中有注释,并添加了一些解释。我希望代码本身简单易懂。我们可以使用链接在摘要和代码清单之间来回切换。
代码清单:-
1a. 我们要做的第一件事是在 Visual Studio 中创建 WPF 项目(我使用了免费在线的 Microsoft Visual Studio Express 2013 for Windows Desktop)。网上有很多关于 WPF 项目的快速入门教程。我们主要做了以下工作:-
- 打开 Visual Studio > 转到文件 > 新建 > 项目
- 在弹出的左侧菜单中,我们选择“模板”>“Visual C#”>“Windows”,然后选择“WPF 应用程序”。
- 将目标应用程序命名为“MvvmCrudGv”,并选择其位置,然后单击“确定”。
(返回摘要)
2a. 完成文件夹创建后,我们的应用程序结构将如下所示:-
(返回摘要)
2b:这是我们的 Todo 类(我们将持久化的实际 DataContract)的外观
[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/MvvmCrudGv.Service.Entity")] public class Todo { private Guid _Id; private string _Title; private string _Text; private DateTime _CreateDt; private DateTime _DueDt; private int _EstimatedPomodori; private int _CompletedPomodori; private string _AddedBy; [DataMember] public Guid Id { get { return _Id; } set { if (!_Id.Equals(value)) { _Id = value; } } } [DataMember] public string Title { get { return _Title; } set { _Title = value; } } [DataMember] public string Text { get { return _Text; } set { _Text = value; } } [DataMember] public DateTime CreateDt { get { return _CreateDt; } set { _CreateDt = value; } } [DataMember] public DateTime DueDt { get { return _DueDt; } set { _DueDt = value; } } [DataMember] public int EstimatedPomodori { get { return _EstimatedPomodori; } set { _EstimatedPomodori = value; } } [DataMember] public int CompletedPomodori { get { return _CompletedPomodori; } set { _CompletedPomodori = value; } } [DataMember] public string AddedBy { get { return _AddedBy; } set { _AddedBy = value; } } public Todo() { _Id = Guid.NewGuid(); _CreateDt = DateTime.Now; _EstimatedPomodori = 1; _CompletedPomodori = 0; _DueDt = DateTime.Today.AddDays(1); } }
(返回摘要)
2c:下面是我们用于视图中的模拟代码,将列表项的属性绑定到 ListView。这里是 ListView,ItemsSource 直接绑定到 TodoList。项目内部绑定到属性。
<ListView ItemsSource="{Binding}" Grid.Row="0" Grid.Column="0"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Title}" /> <TextBlock Text="{Binding Text}" /> <TextBlock Text="{Binding CreateDt}" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
(返回摘要)
2d: 这是我们最初的 MainWindow 类,带有我们的模拟 TodoList。我们刚刚向 TodoList 添加了一些项目。此列表绑定到我们上面的视图。添加此代码后,我们检查它是否有效。每次进行更改时,我们都会不断检查此显示。我们这里不编写测试用例(是的,我知道这是一个不好的做法。我们应该将其移至我们附近的目标。目前,我们大部分测试将直接通过用户测试。)
public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; } public MainWindow() { TodoList = new List<MvvmCrudGv.Service.Entity.Todo>(); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" }); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" }); this.DataContext = TodoList; InitializeComponent(); }
(返回摘要)
4a. 这是基本的 MainWindowViewModel 类。很快我们就会把上面的 TodoList 移到这里。
public class MainWindowViewModel(){}
(返回摘要)
5a. 我们的 ViewModelLocator。这是我们的通用位置,视图将在此处绑定以查找其 ViewModel。
class ViewModelLocator
{
public MainWindowViewModel MainWindowViewModel { get { return new MainWindowViewModel(); } }
// public HomeViewModel HomeViewModel { get { return new HomeViewModel(); } }
// public TodoViewModel TodoDetailsViewModel { get { return new TodoViewModel(); } }
}
(返回摘要)
5b. 在 ViewModelLocator 类中,我们公开了一个名为 MainWindowViewModel 的属性 - 我们的 MainWindow.xaml(视图)将绑定到 ViewModelLocator 的此属性。我们直接在此处返回 MainWindowViewModel 类的一个实例。在实际应用中,我们更倾向于使用适当配置的依赖注入。
public MainWindowViewModel MainWindowViewModel { get { return new MainWindowViewModel(); } }
(返回摘要)
5c. 这是我们在 App.xaml 中声明 ViewModelLocator 实例并给它一个“Locator”键的方式,这样我们的视图就可以绑定到这个类。
<ResourceDictionary> <vm:ViewModelLocator x:Shared="False" x:Key="Locator" xmlns:vm="clr-namespace:MvvmCrudGv.Common" /> <ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
(返回摘要)
5d. 这就是我们在视图中使用上述 ViewModelLocator 实例的方式。我们将把这行代码添加到“MainWindow.xaml”的最顶部。
DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"
(返回摘要)
5e. 由于我们的绑定现在是针对“MainWindowViewModel”类而不是 TodoList,因此我们将绑定到 MainWindowViewModel 类中的“TodoList”属性。这就是我们的 ListView 现在在“MainWindow.xaml”中绑定的方式
ListView ItemsSource="{Binding TodoList}"
(返回摘要)
5f. 下面是我们如何将代码从 MainWindow.xaml.cs 移动到 MainWindowViewModel。
public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; } public MainWindowViewModel() { TodoList = new List<MvvmCrudGv.Service.Entity.Todo>(); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" }); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" }); }
(返回摘要)
6a. 这是我们的 EventAggregator。我们可以将 EventAggregator 视为我们应用程序中所有通信的公共场所。
我们可以将 EventAggregator 想象成一个电视/网络有线连接运营商。有许多发布者一直在持续发布不同类型的事件(或节目)。这取决于我们,我们想订阅哪种类型的事件(或节目)。EventAggregator 维护所有订阅的日志,并同时处理所有事件。一旦事件发布(或节目播出),如果我们已经订阅了该事件,我们的相应处理程序就会被调用(当我们订阅时,我们已经告诉 EventAggregator 应该调用哪个处理程序)。
此外,事件的发布和订阅(或附加处理程序)在 Microsoft 中有一个技巧。Sacha Barber 在他的文章中对此进行了更详细的描述,正如他指出的 Josh Smith(Mvvm 的资深工匠)的链接所述。基本上,一旦我们订阅了一个事件(通过附加处理程序),由于我们的处理程序紧紧地(强引用)持有该事件,即使我的处理程序不再需要它,垃圾回收器也无法将其释放。
因此,微软提供了一个“WeakReference”类来包装我们的处理程序。这就是下面代码清单底部“WeakActionRef”类所做的事情。
此外,正如我们提到的,我们的 EventAggregator 类维护一个事件字典(通过事件“类型”映射到“处理程序列表”(当然是包装在 WeakActionRef 中的))。其余的发布和订阅代码只是像下面注释的那样从这个字典中添加和删除。
/// <summary> /// Contract for EventAggregator /// </summary> public interface IEventAggregator { //Any class should be able toSubscribe to an event type void Subscribe<T>(Action<T> handler); //Any class should be able to Unsubscribe from an event type void Unsubscribe<T>(Action<T> handler); //Any class should be able to Publish an event type void Publish<T>(T evt); } /// <summary> /// This class maintains a dictionary of Events by their "Type" and /// a WeakReference to corresponding "event handlers" logged by Subscribers to that event. /// Any class can publish an event by loggint it here first. /// Any class can subscribe to events of particular type logging their subscription here. /// </summary> public sealed class EventAggregator : IEventAggregator { private Dictionary<Type, List<WeakActionRef>> _subscribers = new Dictionary<Type, List<WeakActionRef>>(); private object _lock = new object(); //Subscribe to an event type public void Subscribe<T>(Action<T> handler) { lock (_lock) { if (_subscribers.ContainsKey(typeof(T))) { //Entry for this event type exists so we add our handler to dictionary var handlers = _subscribers[typeof(T)]; handlers.Add(new WeakActionRef(handler)); } else { //Dictionary entry for this event type is empty so create new key and add handler to it var handlers = new List<WeakActionRef>(); handlers.Add(new WeakActionRef(handler)); _subscribers[typeof(T)] = handlers; } } } //Unsubscribe from an event type public void Unsubscribe<T>(Action<T> handler) { lock (_lock) { if (_subscribers.ContainsKey(typeof(T))) { var handlers = _subscribers[typeof(T)]; //Find out the targetReference to be removed WeakActionRef targetReference = null; foreach (var reference in handlers) { var action = (Action<T>)reference.Target; if ((action.Target == handler.Target) && action.Method.Equals(handler.Method)) { targetReference = reference; break; } } //Remove the targetReference handlers.Remove(targetReference); //If there are no more handlers/subscribers for this event type if (handlers.Count == 0) { _subscribers.Remove(typeof(T)); } } } } //Publish an event type public void Publish<T>(T evt) { lock (_lock) { if (_subscribers.ContainsKey(typeof(T))) { var handlers = _subscribers[typeof(T)]; foreach (var handler in handlers) { if (handler.IsAlive) { //If the handler is still alive Invoke it ((Action<T>)handler.Target).Invoke(evt); } else { //Otherwise just remove the handler from dictionary handlers.Remove(handler); } } //If the number of handlers is zero, remove empty Type entry from Dictionary if (handlers.Count == 0) { _subscribers.Remove(typeof(T)); } } } } } /// <summary> /// A wrapper to handler. Wraps it in WeakReference /// so that we can check the WeakReference every time /// and remove it if it isn't alive anymore /// </summary> public sealed class WeakActionRef { private WeakReference WeakReference { get; set; } public Delegate Target { get; private set; } public bool IsAlive { get { return WeakReference.IsAlive; } } //At creation maintain a weakreference to the target public WeakActionRef(Delegate action) { Target = action; WeakReference = new WeakReference(action.Target); } }
(返回摘要)
6b. 这是我们发布的一种事件类型,或消息类型。它被称为 NavMessage,因为我们在这里会注意到,我们传递了一些与导航相关的参数。基本上,我们的 MainWindow 将订阅此 NavMessage 并为我们执行导航。在更复杂的框架中,我们可能有适当的契约,我们的窗口和页面通常继承自这些契约,以便导航服务可以在它们之间执行导航操作。在这里,我们尽量保持简单——我们的导航系统仍然能够使用 NavMessage 进行导航。(所以,在系统中发布 NavMessage 之前要小心,因为系统一旦检测到此消息将自动寻求导航。我们的 MainWindow 订阅此消息并相应地导航 MainFrame)
/// <summary> /// Used to Publish/Subscribe a Navigation event /// </summary> public class NavMessage { private string Notification; public string PageName { get { return this.Notification; } } public Dictionary<string, string> QueryStringParams { get; private set; } public object NavigationStateParams { get; private set; } public object ViewObject { get; private set; } public NavMessage(string pageName) { this.Notification = pageName; } public NavMessage(string pageName, Dictionary<string, string> queryStringParams) : this(pageName) { QueryStringParams = queryStringParams; } //Pass the instance of the View class and the ViewModel public NavMessage(object viewObject, object navigationStateParams) : this(viewObject.GetType().Name) { ViewObject = viewObject; NavigationStateParams=navigationStateParams; } } public class ObjMessage { public string Notification { get; private set; } public object PayLoad { get; private set; } public ObjMessage(string pageName, object payLoad) { Notification = pageName; PayLoad = payLoad; } }
(返回摘要)
7c. 我们将虚拟 ListView 从 MainWindow.xaml 移动到 Home.xaml。这里没有任何变化,我们只是移动了 ListView。
<Grid> <ListView ItemsSource="{Binding TodoList}" Grid.Row="0" Grid.Column="0"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Title}" /> <TextBlock Text="{Binding Text}" /> <TextBlock Text="{Binding CreateDt}" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid>
(返回摘要)
7d. 这是我们的 HomeViewModel 的样子。正如我们在这里看到的,没有什么新内容,只是将 MainWindowViewModel 的虚拟列表移动到了 HomeViewModel。
public class HomeViewModel { public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; } public HomeViewModel() { TodoList = new List<MvvmCrudGv.Service.Entity.Todo>(); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" }); TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" }); } }
(返回摘要)
7e. 这是我们的 MainWindow.xaml 现在的样子。正如我们在这里注意到的,我们有一个简单的 Grid,包含两行。第一行包含菜单,第二行包含一个 Frame。简单,没什么花哨的。
<Window x:Class="MvvmCrudGv.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MVVM WCF CRUD Operations" Height="350" Width="525" DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="26"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Name="grdMenuNav" Height="26"> <Border BorderBrush="Gray" BorderThickness="1"> <Menu IsMainMenu="True" Grid.Row="0" Grid.Column="0" Margin="0" Padding="5,0" Height="22" Background="White"> <MenuItem Header="_File" Height="22"> <MenuItem Header="_Exit" Command="{Binding ExitCmd}" /> </MenuItem> </Menu> </Border> </Grid> <DockPanel Grid.Row="1" Width="Auto"> <Frame x:Name="_MainFrame" NavigationUIVisibility="Hidden" /> </DockPanel> </Grid> </Window>
(返回摘要)
7f. 这些是添加到 ViewModelLocator 的属性,它将暴露 HomeViewModel 和 TodoViewModel。我们的各自视图(Home.xaml 和 TodoDetails.xaml)将绑定到这些属性。
public HomeViewModel HomeViewModel { get { return new HomeViewModel(); } } public TodoViewModel TodoDetailsViewModel { get { return new TodoViewModel(); } }
(返回摘要)
7g. 以下是我们将添加到 Home.xaml 和 TodoDetails.xaml 以绑定到上述属性的代码行。
DataContext="{Binding HomeViewModel, Source={StaticResource Locator}}" DataContext="{Binding TodoDetailsViewModel, Source={StaticResource Locator}}"
(返回摘要)
7h. 我们向应用程序添加了一个静态 EventAggregator。正如我们在这里注意到的,我们在应用程序启动时实例化了它。由于它是静态的,因此在不使用时不会被释放。我们大部分时间都会用它来处理所有通信,因此我们将其设置为静态并放在应用程序级别。
public partial class App : Application { public static IEventAggregator eventAggregator { get; private set; } protected override void OnStartup(StartupEventArgs e) { //Common.BootStrapper.Instance.Bootstrap(this,e); eventAggregator = new EventAggregator(); base.OnStartup(e); } protected override void OnExit(ExitEventArgs e) { //Common.BootStrapper.Instance.ShutDown(this, e); base.OnExit(e); } }
(返回摘要)
7i. 注意这里我们的 MainWindow 如何订阅 NavMessage。一旦我们的 MainWindow 通过 EventAggregator 通道接收到“NavMessage”通知,它就会根据 NavMessage 中提到的目标导航“MainFrame”。一旦 MainFrame 被导航(Container_LoadCompleted),MainWindow 会发布带有预期负载的 ObjMessage。目标 ViewModel 然后可以订阅 ObjMessage 以接收负载,从而由 MainWindow 重定向。
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
IEventAggregator _eventAggregator;
public MainWindow()
{
_eventAggregator = App.eventAggregator;
InitializeComponent();
_MainFrame.NavigationService.LoadCompleted += new LoadCompletedEventHandler(container_LoadCompleted);
_MainFrame.NavigationService.Navigate(new Home());
_eventAggregator.Subscribe<NavMessage>(NavigateToPage);
}
private void NavigateToPage(NavMessage message)
{
object viewObject = message.ViewObject;
object navigationState = message.NavigationStateParams;
if ((viewObject!=null)&&(navigationState!=null))
{
_MainFrame.NavigationService.Navigate(viewObject, navigationState);
return;
}
else if (viewObject!=null)
{
_MainFrame.NavigationService.Navigate(viewObject);
return;
}
//Silverlight
string queryStringParams = message.QueryStringParams == null ? "" : GetQueryString(message);
string uri = string.Format("/Views/{0}.xaml{1}", message.PageName, queryStringParams);
_MainFrame.NavigationService.Navigate(new Uri(uri, UriKind.Relative));
}
void container_LoadCompleted(object sender, NavigationEventArgs e)
{
if (e.ExtraData != null)
_eventAggregator.Publish<ObjMessage>(new ObjMessage(e.Content.GetType().Name, e.ExtraData));
}
private string GetQueryString(NavMessage message)
{
string qstr = null;
if (message.QueryStringParams != null)
{
qstr = string.Concat(message.QueryStringParams.Select(x => x.Key + "=" + x.Value).ToList<string>().ToArray());
qstr = "?" + qstr;
}
return (qstr);
}
}
(返回摘要)
8a. 下面是我们的服务层。我们有我们的“ServiceContract”或协议,我们将能够通过它与我们的服务类“ITodoService”对话,其中我们有基本的 CRUD 操作。我们的下面“TodoService”的实现实现了上述契约,目前我们只是将我们的待办事项存储在内存列表中,如下所示。我们很快将用实际的持久化替换这个内存列表。但目前,这应该能让我们的应用程序运行起来。
[ServiceContract] public interface ITodoService { [OperationContract] Guid Add(Todo todo); [OperationContract] void Delete(Guid id); [OperationContract] List<Todo> List(); [OperationContract] bool Update(Todo todo); [OperationContract] Todo Get(Guid id); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, UseSynchronizationContext = false)] public class TodoService:ITodoService { List<Todo> _lstDb = new List<Todo>(); public Guid Add(Todo todo) { _lstDb.Add(todo); return (todo.Id); } public Todo Get(Guid id) { return (_lstDb.Where(x => x.Id.ToString() == id.ToString()).FirstOrDefault()); } public void Delete(Guid id) { _lstDb.Remove(_lstDb.Where(x => x.Id.ToString() == id.ToString()).FirstOrDefault()); } public List<Todo> List() { return (_lstDb); } public bool Update(Todo todo) { var itm = _lstDb.Where(x => x.Id == todo.Id).FirstOrDefault(); if (itm == null) { return false; } else { _lstDb[_lstDb.IndexOf(itm)] = todo; return (true); } } }
(返回摘要)
9a. 这是我们的 BootStrapper 类。如上所述,它是一个简单的单例。为什么是单例?我们希望为我们的启动和关闭例程提供一个公共实例。正如我们在这里注意到的,我们在这里实例化了 TodoService 的另一个静态实例。在实际应用中,我们宁愿使用依赖注入,并且 DI 会在我们需要的任何地方实例化它。我们不会将此服务类放在 BootStrapper 中,但由于我们无论如何都需要一个实例,因此我们在此处创建了一个可重用的静态实例,如下所示。
class BootStrapper { private static BootStrapper _instance; private static ITodoService _todoService; public ITodoService TodoService { get { return (_todoService); } } private BootStrapper() { _todoService = new TodoService(); } public static BootStrapper Instance { get { if (_instance==null) { _instance = new BootStrapper(); } return (_instance); } } public void Bootstrap(App app, System.Windows.StartupEventArgs e) { //Do bootstap here } public void ShutDown(App app, System.Windows.ExitEventArgs e) { //Do shutdown cleanup here } }
(返回摘要)
9c. 这个虚拟填充器代码我们可以移到 TodoService 类中,并检查它是否返回一个列表。
if ((_lstDb==null)||(_lstDb.Count==0)) { _lstDb.Add(new Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" }); _lstDb.Add(new Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" }); }
(返回摘要)
9d. 在我们的 HomeViewModel 中,我们将绑定到 TodoService 返回的列表。
public HomeViewModel() { TodoList = MvvmCrudGv.Common.BootStrapper.Instance.TodoService.List(); }
(返回摘要)
10a. 我们的 BaseViewModel 类。如上所述,它实现了 INotifyPropertyChanged,如果任何属性被修改,它会触发 OnPropertyChanged。这个类还包含一个 IsDirty 布尔属性,一旦任何属性被修改,我们就会将其标记为 true——这样我们就能知道何时一个对象已被修改。
public class BaseViewModel : INotifyPropertyChanged { private bool _IsDirty; public virtual bool IsDirty { get { return _IsDirty; } set { if (_IsDirty != value) { _IsDirty = value; OnPropertyChanged("IsDirty"); } } } public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// When property is changed call this method to fire the PropertyChanged Event /// </summary> /// <param name="propertyName"></param> public void OnPropertyChanged(string propertyName) { //Fire the PropertyChanged event in case somebody subscribed to it if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); if (!propertyName.Equals("IsDirty")) { IsDirty = true; } } } public void OnPropertyChanged<T>(Expression<Func<T>> property) { if (PropertyChanged != null) { var memberExpression = property.Body as MemberExpression; PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name)); if (!memberExpression.Member.Name.Equals("IsDirty")) { IsDirty = true; } } } }
(返回摘要)
10b. 这是 RelayCommand 类。这是一个非常常见的做法,这个类的目的是将命令中继到我们将实例化它的适当 Action。
/// <summary> /// A command whose sole purpose is to /// relay its functionality to other /// objects by invoking delegates. The /// default return value for the CanExecute /// method is 'true'. /// </summary> public class RelayCommand : ICommand { #region Fields readonly Action<object> _execute; readonly Predicate<object> _canExecute; #endregion // Fields #region Constructors /// <summary> /// Creates a new command that can always execute. /// </summary> /// <param name="execute">The execution logic.</param> public RelayCommand(Action<object> execute) : this(execute, null) { } /// <summary> /// Creates a new command. /// </summary> /// <param name="execute">The execution logic.</param> /// <param name="canExecute">The execution status logic.</param> public RelayCommand(Action<object> execute, Predicate<object> canExecute) { if (execute == null) throw new ArgumentNullException("execute"); _execute = execute; _canExecute = canExecute; } #endregion // Constructors #region ICommand Members [DebuggerStepThrough] public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); } public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } public void Execute(object parameter) { _execute(parameter); } #endregion // ICommand Members }
(返回摘要)
10c. 以下是一些我们通常在 WPF 中需要的实用转换器。基本上是从布尔值转换为 Visibility,布尔值转换为 Visibility 的反向,我们还有一个 Enum EditMode,所以我们也想将我们的枚举值转换为 Visibility。然后我们有一个转换器来处理列表中的长字符串。因为长字符串在我们的列表中可能不太令人愉快,并且可能使其变得不均匀,所以我们创建了一个转换器来截断和美化字符串以供我们的 ListView 使用。
public enum EditMode { Create, Update } public class InverseBooleanConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType != typeof(Visibility)) throw new InvalidOperationException("The target must be a boolean"); return (!((bool)value)); } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotSupportedException(); } #endregion } public class BooleanToVisibilityConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType != typeof(Visibility)) throw new InvalidOperationException("The target must be a boolean"); if ((bool)value) { return Visibility.Visible; } return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotSupportedException(); } #endregion } public class InverseBooleanToVisibilityConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType != typeof(Visibility)) throw new InvalidOperationException("The target must be a boolean"); if (!(bool)value) { return Visibility.Visible; } return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotSupportedException(); } #endregion } public class EditModeToVisibilityConverter : IValueConverter { #region IValueConverter Members public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (targetType != typeof(Visibility)) throw new InvalidOperationException("The target must be a boolean"); EditMode mode= (EditMode)Enum.Parse(typeof(EditMode),parameter.ToString()); if(((EditMode)value).Equals(mode)) { return Visibility.Visible; } return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotSupportedException(); } #endregion } public class TextTruncateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value == null) return string.Empty; if (parameter == null) return value; int _MaxLength; if (!int.TryParse(parameter.ToString(), out _MaxLength)) return value; var _String = value.ToString().Replace("\r\n", "... ").Replace("\n", "... ").Replace("\r", "... "); if (_String.Length > _MaxLength) _String = _String.Substring(0, _MaxLength) + "..."; return _String; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
(返回摘要)
10d. 这是我们的 ResourceDictionary。在这里,我们实例化了我们的大部分转换器,以便我们可以在任何需要的地方使用它们。我们还在这里添加了一些可重用样式。我们将注意到我们的 ResourceDictionary 底部有一个 ErrorTemplate,我们将用它来显示我们的验证消息。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:MvvmCrudGv.Common"> <c:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter" /> <c:InverseBooleanToVisibilityConverter x:Key="inverseBooleanToVisibilityConverter" /> <c:EditModeToVisibilityConverter x:Key="editModeToVisibilityConverter" /> <c:TextTruncateConverter x:Key="textTruncateConverter" /> <Style TargetType="{x:Type Border}" x:Key="grayBgBorder"> <!-- All rows --> <Setter Property="BorderBrush" Value="Gray" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="CornerRadius" Value="0" /> <Setter Property="Background" Value="LightGray" /> </Style> <Style TargetType="{x:Type Border}" x:Key="grayBorder"> <!-- All rows --> <Setter Property="BorderBrush" Value="Gray" /> <Setter Property="BorderThickness" Value="1" /> <Setter Property="CornerRadius" Value="0" /> </Style> <ControlTemplate x:Key="textBoxErrorTemplate"> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Bottom" Foreground="Orange" FontSize="12pt">**</TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder /> </Border> </DockPanel> </ControlTemplate> <!-- this is default error template for all textboxes unless we choose custom one--> <Style TargetType="{x:Type TextBox}"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Bottom" Foreground="Orange" FontSize="12pt" Text="{Binding ElementName=MyErrorAdorner,Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" Visibility="{Binding XPath=AdornedElement.(Validation.HasErrors), Converter={StaticResource ResourceKey=booleanToVisibilityConverter}}"> </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="MyErrorAdorner" /> </Border> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> </ResourceDictionary>
(返回摘要)
11a. 如上所述,这是我们的 TodoViewModel,它只是基本实体“Todo”的一个包装器。它暴露了基本“Todo”实体的所有属性,并用一些 MVVM 特定的注解进行包装。它还包含一些 MVVM 需要在 ViewModel 上操作的命令。
public class TodoViewModel : BaseViewModel { public Todo _todo { get; private set; } public TodoViewModel() : this(new Todo()) { } public TodoViewModel(Todo todo) { _todo = todo; } #region Properties public override bool IsDirty { get { return base.IsDirty; } set { if (base.IsDirty != value) { base.IsDirty = value; OnPropertyChanged("IsDirty"); } } } public Guid Id { get { return _todo.Id; } set { _todo.Id = value; OnPropertyChanged("Id"); } } public string Title { get { return _todo.Title; } set { _todo.Title = value; OnPropertyChanged("Title"); } } public string Text { get { return _todo.Text; } set { _todo.Text = value; OnPropertyChanged("Text"); } } public DateTime CreateDt { get { return _todo.CreateDt; } //set { _todo.CreateDt = value; //OnPropertyChanged("CreateDt"); //} } public DateTime DueDt { get { return _todo.DueDt; } set { _todo.DueDt = value; OnPropertyChanged("DueDt"); } } public int EstimatedPomodori { get { return _todo.EstimatedPomodori; } set { _todo.EstimatedPomodori = value; OnPropertyChanged("EstimatedPomodori"); } } public int CompletedPomodori { get { return _todo.CompletedPomodori; } set { _todo.CompletedPomodori = value; OnPropertyChanged("CompletedPomodori"); } } public string AddedBy { get { return _todo.AddedBy; } set { _todo.AddedBy = value; OnPropertyChanged("AddedBy"); } } #endregion }
(返回摘要)
12a. 这是我们的 Home.xaml。这是我们的 CRUD ListView。我们将注意到这里有一个 Grid 布局,包含三行。第一行包含我们的 CRUD 列表的标题。第二行包含实际列表。第三行再次包含一个 Grid,其中包含我们的编辑文本框和按钮的水平 StackPanel。我们使用我们的转换器来切换按钮的可见性。(我们在这里注释掉了一些代码。
<Page x:Class="MvvmCrudGv.Views.Home" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Title="Home" DataContext="{Binding HomeViewModel, Source={StaticResource Locator}}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="24"></RowDefinition> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="50"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Grid Grid.Row="0" Grid.Column="0"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> </Grid.ColumnDefinitions> <Border Grid.Row="0" Grid.Column="0" Style="{StaticResource grayBgBorder}"></Border> <Border Grid.Row="0" Grid.Column="1" Style="{StaticResource grayBgBorder}"></Border> <Border Grid.Row="0" Grid.Column="2" Style="{StaticResource grayBgBorder}"></Border> <Border Grid.Row="0" Grid.Column="3" Style="{StaticResource grayBgBorder}"></Border> <TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center"> <Bold> Title </Bold> </TextBlock> <TextBlock Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center"> <Bold> Notes </Bold> </TextBlock> <TextBlock Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center"> <Bold> Estim. </Bold> </TextBlock> <TextBlock Grid.Row="0" Grid.Column="3" HorizontalAlignment="Center"> <Bold> Cmplt. </Bold> </TextBlock> </Grid> <ListView ItemsSource="{Binding TodoList}" Grid.Row="1" Grid.Column="0" SelectedItem="{Binding SelectedTodo}" SelectedIndex="{Binding TodoListSelectedIndex}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" ><!--:EventToCommand.Command="{Binding GoTodoDetailsCmd}" b:EventToCommand.CommandParameter="{Binding SelectedTodo}" xmlns:b="clr-namespace:MvvmCrudGv.Common.Behaviors">--> <ListView.ItemTemplate> <DataTemplate> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> </Grid.ColumnDefinitions> <Border Grid.Row="0" Grid.ColumnSpan="4" Visibility="{Binding IsDirty,Converter={StaticResource booleanToVisibilityConverter}}" BorderBrush="Red" BorderThickness="1"/> <TextBlock Text="{Binding Title}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center" /> <TextBlock Text="{Binding Text, Converter={StaticResource textTruncateConverter},ConverterParameter=32}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" /> <TextBlock Text="{Binding EstimatedPomodori}" Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center" /> <TextBlock Text="{Binding CompletedPomodori}" Grid.Row="0" Grid.Column="3" HorizontalAlignment="Center" /> </Grid> </DataTemplate> </ListView.ItemTemplate> </ListView> <Grid Grid.Row="2" Grid.Column="0"> <Grid.RowDefinitions> <RowDefinition Height="24"></RowDefinition> <RowDefinition Height="24"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> <ColumnDefinition Width="50"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBox Text="{Binding SelectedTodo.Title}" Grid.Row="0" Grid.Column="0" /> <TextBox Text="{Binding SelectedTodo.Text}" Grid.Row="0" Grid.Column="1" /> <TextBox Grid.Row="0" Grid.Column="2"> <Binding Path="SelectedTodo.EstimatedPomodori"> <Binding.ValidationRules> <!--<c:NumericValidator></c:NumericValidator>--> </Binding.ValidationRules> </Binding> </TextBox> <TextBox Grid.Row="0" Grid.Column="3" > <Binding Path="SelectedTodo.CompletedPomodori"> <Binding.ValidationRules> <!--<c:NumericValidator></c:NumericValidator>--> </Binding.ValidationRules> </Binding> </TextBox> <StackPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Right"> <Button Command="{Binding NewTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="New"></Button> <Button Command="{Binding AddTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Create}" Content="Add"></Button> <Button Command="{Binding UpdateTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Save"></Button> <Button Command="{Binding GoTodoDetailsCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Details"></Button> <Button Command="{Binding DeleteTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Delete"></Button> </StackPanel> </Grid> </Grid> </Page>
(返回摘要)
12b. 这是我们的 HomeViewModel 的样子。正如我们在这里注意到的,它包含一些基本的 CRUD 命令。然后我们有一个 ObservableCollection<TodoViewModel> TodoList,我们将其绑定到我们的 ListView 显示。然后我们有一个“SelectedTodo”属性。一旦我们从 ListView 中选择了一个项目,我们就会将其引用标记为“SelectedTodo”。然后我们可以在 SelectedTodo 上执行我们的编辑操作,相应的 ListView 项目将反映更改。如果单击“新建”,SelectedTodo 将实例化一个新的空 TodoViewModel,我们可以编辑并“保存”它。
public class HomeViewModel : BaseViewModel { public ICommand AddTodoCmd { get; private set; } public ICommand ListTodosCmd { get; private set; } public ICommand UpdateTodoCmd { get; private set; } public ICommand LoadTodoCmd { get; private set; } public ICommand DeleteTodoCmd { get; private set; } public ICommand NewTodoCmd { get; private set; } public ICommand GoTodoDetailsCmd { get; private set; } public ObservableCollection<TodoViewModel> TodoList { get; set; } private TodoViewModel _SelectedTodo; private int _TodoListSelectedIndex; private EditMode _TodoListEditMode; private IEventAggregator _eventAggregator; public EditMode TodoListEditMode { get { return _TodoListEditMode; } set { if (_TodoListEditMode != value) { _TodoListEditMode = value; OnPropertyChanged("TodoListEditMode"); } } } public int TodoListSelectedIndex { get { return _TodoListSelectedIndex; } set { if (_TodoListSelectedIndex != value) { _TodoListSelectedIndex = value; if (_TodoListSelectedIndex == -1) { TodoListEditMode = EditMode.Create; } else { TodoListEditMode = EditMode.Update; } OnPropertyChanged("TodoListSelectedIndex"); } } } public TodoViewModel SelectedTodo { get { return _SelectedTodo; } set { if ((null != value) && (_SelectedTodo != value)) { _SelectedTodo = value; OnPropertyChanged("SelectedTodo"); } } } MvvmCrudGv.Service.ITodoService _todoServiceClient; public HomeViewModel() { _todoServiceClient = BootStrapper.Instance.TodoService; _eventAggregator = App.eventAggregator; TodoList = new ObservableCollection<TodoViewModel>(); loadTodoList(); _TodoListSelectedIndex = -1; _SelectedTodo = new TodoViewModel(); UpdateTodoCmd = new RelayCommand(ExecUpdateTodo, CanUpdateTodo); DeleteTodoCmd = new RelayCommand(ExecDeleteTodo, CanDeleteTodo); LoadTodoCmd = new RelayCommand(ExecLoadTodo, CanLoadTodo); ListTodosCmd = new RelayCommand(ExecListTodos, CanListTodos); AddTodoCmd = new RelayCommand(ExecAddTodo, CanAddTodo); NewTodoCmd = new RelayCommand(ExecNewTodo, CanNewTodo); GoTodoDetailsCmd = new RelayCommand(ExecGoTodoDetails, CanGoTodoDetails); } private void ExecNewTodo(object obj) { SelectedTodo = new TodoViewModel(); TodoListSelectedIndex = -1; } [DebuggerStepThrough] private bool CanNewTodo(object obj) { return (true); } private void loadTodoList() { //Dummy if (!(_todoServiceClient.List().Count > 0)) { var tid = _todoServiceClient.Add(new Service.Entity.Todo() { AddedBy = "Amit", Title = "First todo", Text = "this is first todo" }); } var lstTodos = _todoServiceClient.List(); if ((lstTodos != null) && (lstTodos.Count > 0)) { foreach (var item in lstTodos) { TodoList.Add(new TodoViewModel(item)); } } } //This goes in Initialization/constructor private void ExecDeleteTodo(object obj) { System.Windows.MessageBoxResult confirmRunResult = System.Windows.MessageBox.Show("Are you sure you want to delete this todo?", "Delete Item?", System.Windows.MessageBoxButton.OKCancel); if (confirmRunResult == System.Windows.MessageBoxResult.Cancel) { return; } _todoServiceClient.Delete(SelectedTodo.Id); TodoList.Remove(SelectedTodo); resetSelectedTodo(); } private bool CanDeleteTodo(object obj) { return (true); } //This goes in Initialization/constructor private void ExecLoadTodo(object obj) { } private bool CanLoadTodo(object obj) { return (true); } //This goes in Initialization/constructor private void ExecUpdateTodo(object obj) { bool isok = _todoServiceClient.Update(SelectedTodo._todo); SelectedTodo.IsDirty = !isok; } private bool CanUpdateTodo(object obj) { return (true); } //This goes in Initialization/constructor private void ExecListTodos(object obj) { } private bool CanListTodos(object obj) { return (true); } //This goes in Initialization/constructor private void ExecAddTodo(object obj) { Guid addedid = _todoServiceClient.Add(SelectedTodo._todo); SelectedTodo.Id = addedid; SelectedTodo.IsDirty = false; TodoList.Add(SelectedTodo); resetSelectedTodo(); } private void resetSelectedTodo() { SelectedTodo = new TodoViewModel(); TodoListSelectedIndex = -1; } private bool CanAddTodo(object obj) { return (true); } private void ExecGoTodoDetails(object obj) { _eventAggregator.Publish<NavMessage>(new NavMessage(new MvvmCrudGv.Views.TodoDetails(), SelectedTodo)); } private bool CanGoTodoDetails(object obj) { return (true); } }
(返回摘要)
13a. 这是我们的 TodoDetails 页面:-
<Page x:Class="MvvmCrudGv.Views.TodoDetails"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="TodoDetails"
xmlns:c="clr-namespace:MvvmCrudGv.Common"
DataContext="{Binding TodoDetailsViewModel, Source={StaticResource Locator}}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="24"></RowDefinition>
<RowDefinition Height="24"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" Visibility="{Binding IsDirty,Converter={StaticResource booleanToVisibilityConverter}}" BorderBrush="Red" BorderThickness="1"/>
<Label Grid.Row="0" Grid.Column="0" >Title</Label>
<Label Grid.Row="1" Grid.Column="0" >Notes</Label>
<Label Grid.Row="2" Grid.Column="0" >Estimated Hrs</Label>
<Label Grid.Row="3" Grid.Column="0" >Completed Hrs</Label>
<TextBox Text="{Binding CurrentTodo.Title}" Grid.Row="0" Grid.Column="1" />
<TextBox Text="{Binding CurrentTodo.Text}" Grid.Row="1" Grid.Column="1" VerticalContentAlignment="Stretch" Height="Auto" AcceptsReturn="True" />
<TextBox Grid.Row="2" Grid.Column="1">
<Binding Path="CurrentTodo.EstimatedPomodori">
<Binding.ValidationRules>
<c:NumericValidator></c:NumericValidator>
</Binding.ValidationRules>
</Binding>
</TextBox>
<TextBox Grid.Row="3" Grid.Column="2" >
<Binding Path="CurrentTodo.CompletedPomodori">
<Binding.ValidationRules>
<c:NumericValidator></c:NumericValidator>
</Binding.ValidationRules>
</Binding>
</TextBox>
<StackPanel Grid.Row="4" Grid.ColumnSpan="2" HorizontalAlignment="Right" Orientation="Horizontal">
<Button Command="{Binding GoBackCmd}" Content="Back" Width="50"></Button>
<Button Command="{Binding SaveTodoCmd}" Content="Save" Width="50"></Button>
</StackPanel>
</Grid>
</Page>
(返回摘要)
13b. 请注意这里我们的 TodoDetailsViewModel 如何订阅 ObjMessage,一旦它从 ObjMessage 的 Payload 中收到目标 TodoViewModel,TodoDetailsViewModel 就会相应地更新自身:-
public class TodoDetailsViewModel: BaseViewModel
{
private TodoViewModel _CurrentTodo;
IEventAggregator _eventAggregator;
public ICommand GoBackCmd { get; private set; }
private readonly ICommand _SaveTodoCmd;
public ICommand SaveTodoCmd { get { return (_SaveTodoCmd); } }
public TodoViewModel CurrentTodo
{
get { return _CurrentTodo; }
set
{
if ((null != value) && (_CurrentTodo != value))
{
_CurrentTodo = value;
OnPropertyChanged("CurrentTodo");
}
}
}
public TodoDetailsViewModel()
{
_eventAggregator = App.eventAggregator;
_eventAggregator.Subscribe(UpdateTodo);
//This goes in Initialization/constructor
GoBackCmd = new RelayCommand(ExecGoBack, CanGoBack);
_SaveTodoCmd = new RelayCommand(ExecSaveTodo, CanSaveTodo);
}
private void UpdateTodo(MvvmCrudGv.Views.ObjMessage message)
{
if(message.Notification.Equals("TodoDetails")){
var td = (TodoViewModel)message.PayLoad;
CurrentTodo = td;
}
}
#region Commands
private void ExecGoBack(object obj)
{
if (IsDirty)
{
System.Windows.MessageBoxResult confirmRunResult = System.Windows.MessageBox.Show("If you go back the changes will be discarded. Do you want to do this? If not, select 'Cancel' and 'Save' the changes first.", "Discard Changes?", System.Windows.MessageBoxButton.OKCancel);
if (confirmRunResult == System.Windows.MessageBoxResult.Cancel)
{
return;
}
}
App.eventAggregator.Publish(new Views.NavMessage("Home"));
}
private bool CanGoBack(object obj)
{
return (true);
}
private void ExecSaveTodo(object obj)
{
//Todo: Add the functionality for SaveTodoCmd Here
bool isok = BootStrapper.Instance.TodoService.Update(this.CurrentTodo._todo);
IsDirty = !isok;
}
[DebuggerStepThrough]
private bool CanSaveTodo(object obj)
{
//Todo: Add the checking for CanSaveTodo Here
return (IsDirty);
}
#endregion
}
(返回摘要)
14a. 这是我们的 TodoPersistence 层的契约和实现。它们都类似于我们的 TodoService 契约和实现。只是它们在我们的 Protobuf-net 数据库 ProtobufDB 上操作。
interface ITodoPersistence
{
Guid Add(Todo todo);
void Delete(Guid id);
List<Todo> List();
bool Update(Todo todo);
Todo Get(Guid id);
}
class TodoPersistence: ITodoPersistence
{
AbstractCrudDB _protobufDb;
public TodoPersistence()
{
_protobufDb = new ProtobufDB(MvvmCrudGvConstants.DefaultDataPath,"bin");
}
public Guid Add(Entity.Todo todo)
{
var filename = _protobufDb.Write<Todo>(todo, todo.Id.ToString());
return (todo.Id);
}
public void Delete(Guid id)
{
_protobufDb.Delete<Todo>(id.ToString());
}
public List<Entity.Todo> List()
{
return (_protobufDb.Read<Todo>().ToList());
}
public bool Update(Entity.Todo todo)
{
//Nasty isn't it
Guid id = Add(todo);
return (true);
}
public Entity.Todo Get(Guid id)
{
return _protobufDb.Read<Todo>(id.ToString());
}
}
(返回摘要)
14b. 这是我们的持久层。我不会在这里详细介绍。它基本上包含非常基本的 CRUD 操作。我们的数据库是“Protobuf”数据库。我们正在将我们的对象序列化为“Protobuf”序列化对象。我们的 ProdobufDb 基本上使用 Protobuf-net 将我们的对象序列化/反序列化到文件中。没有什么花哨的,其余的只是帮助数据库完成其职责的辅助类。
class ProtobufDB:AbstractCrudDB
{
private static readonly object syncLock = new object();
public ProtobufDB(string basePath,string fileExtension):base(basePath,fileExtension)
{
}
public override string Write<T>(T row, string id)
{
string filename = CreateFilename(typeof(T), id);
lock (syncLock)
{
using (var file = File.Create(filename))
{
Serializer.Serialize(file, row);
}
}
return filename;
}
public override T Read<T>(string id)
{
string filename = CreateFilename(typeof(T), id);
return readFileToType<T>(filename);
}
public override T[] Read<T>()
{
string filePattern = string.Format("{0}-*.{1}", typeof(T).Name, FileExtension);
string[] files = Directory.GetFiles(BasePath, filePattern, SearchOption.TopDirectoryOnly);
List<T> list = new List<T>();
foreach (string filename in files)
{
list.Add(readFileToType<T>(filename));
}
return list.ToArray();
}
public override void Delete<T>(string id)
{
Delete(typeof(T), id);
}
public override void Delete(Type type, string id)
{
string filename = CreateFilename(type, id);
if (File.Exists(filename))
{
File.Delete(filename);
}
}
private T readFileToType<T>(string filename)
{
if (File.Exists(filename))
{
using (var file = File.OpenRead(filename))
{
return (T)Serializer.Deserialize<T>(file);
}
}
return default(T);
}
}
abstract class AbstractCrudDB
{
protected AbstractCrudDB(string basePath,string fileExtension)
{
BasePath = basePath;
}
public string BasePath { get; private set; }
public string FileExtension { get; private set; }
//Create
public abstract string Write<T>(T row, string id);
//Read
public abstract T Read<T>(string id);
public abstract T[] Read<T>();
//Delete
public abstract void Delete<T>(string id);
public abstract void Delete(Type type, string id);
//Update
//No update operation right now
public virtual string CreateID()
{
return Guid.NewGuid().ToString("D");
}
protected string CreateFilename(Type type, string id)
{
return System.IO.Path.Combine(BasePath, string.Format("{0}-{1}.{2}", type.Name, id, FileExtension));
}
}
(返回摘要)
14c. 帮助我们数据库端操作的实用类:-
public class Utility
{
public static string getAbsolutePath(string folder, bool createIfNoDirectory = false)
{
string rtrnPath = Path.Combine(getAppBasePath(), folder);
if ((createIfNoDirectory) && (!System.IO.Directory.Exists(folder)))
{
Directory.CreateDirectory(rtrnPath);
}
return (rtrnPath);
}
public static string getAbsolutePath(string folder, string fileName, bool createIfNoDirectory = false)
{
string rtrnPath = Path.Combine(getAppBasePath(), folder);
if ((createIfNoDirectory) && (!System.IO.Directory.Exists(folder)))
{
Directory.CreateDirectory(rtrnPath);
}
rtrnPath = Path.Combine(rtrnPath, fileName);
return (rtrnPath);
}
public static string getAppBasePath()
{
string codeBase = System.Reflection.Assembly.GetExecutingAssembly().Location;
return Path.GetDirectoryName(codeBase);
}
}
public struct MvvmCrudGvConstants
{
public static string DefaultLogFileName { get { return (Utility.getAbsolutePath("Log", "Error" + DateTimeStampAsString + ".log", true)); } }
public static string DefaultLogFolder { get { return (Utility.getAbsolutePath("Log", true)); } }
public static string DefaultDataPath { get { return (Utility.getAbsolutePath("Data", true)); } }
public static string DefaultConfigPath { get { return (Utility.getAbsolutePath("Config", "MvvmCrudGv.cfg", true)); } }
public static string DateTimeStampAsString { get { return (DateTime.Now.ToString("ddMMMyyyy_hhmm")); } }
}
(返回摘要)
重要链接:-
弱事件内存泄漏问题
- http://diditwith.net/PermaLink,guid,aacdb8ae-7baa-4423-a953-c18c1c7940ab.aspx
- http://msdn.microsoft.com/en-us/library/aa970850.aspx
WPF 验证
- https://codeproject.org.cn/Articles/15239/Validation-in-Windows-Presentation-Foundation
- https://codeproject.org.cn/Articles/321745/WPF-Validation
- https://codeproject.org.cn/Tips/784331/WPF-MVVM-Validation-ViewModel-using-IDataErrorInfo
将属性从视图映射到视图模型
- http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/
- https://codeproject.org.cn/Articles/35277/MVVM-Mediator-Pattern
- http://www.mindscapehq.com/blog/index.php/2012/02/01/caliburn-micro-part-4-the-event-aggregator/
- http://blogs.u2u.be/diederik/post/2011/01/15/Using-the-Prism-40-Event-Aggregator.aspx
- https://catelproject.atlassian.net/wiki/display/CTL/Mapping+properties+from+view+to+view+model
Grid RowDefinitions 和 ColumnDefinitions 的样式设置
稍后添加更多内容。
历史
2014年10月17日:首次发布。
2014年11月3日:更新了导航和导航消息信息。