文章目录
项目简介
本项目想写一个七牛云空间管理的工具,省去每次都要登录网页管理的繁琐,也是为了学习一下WPF开发的MVVM架构和思想,降低代码耦合度(降低UI层和BLL层的代码耦合度)。前后端只要约定好命令名和参数即可,让搞UI的专心搞UI,搞后台的专心写后台逻辑。
项目主要架构
考虑到对XP系统的友好支持,使用了.Net 4
MahApps.Metro
PropertyChanged.Fody
Microsoft.Bcl
Qiniu
新建项目QiniuDiskManager
NuGet安装
Install-Package MahApps.Metro
Install-Package MahApps.Metro.SimpleChildWindow
Install-Package MahApps.Metro.IconPacks -Version 2.3.0
Install-Package Microsoft.Bcl -Version 1.1.8
Install-Package Microsoft.Bcl.Async -Version 1.0.168
Install-Package Microsoft.Bcl.Build -Version 1.0.14
Install-Package Newtonsoft.Json -Version 11.0.1
Install-Package ControlzEx -Version 3.0.2.4
Install-Package Qiniu
Install-Package PropertyChanged.Fody
新建项目结构文件夹
Bases
Commands
Models
Pages
ViewModels
新建必要文件
ViewModelBase.cs
DelegateCommand.cs
using System.ComponentModel;
namespace QiniuDiskManager.Bases
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
namespace QiniuDiskManager.Commands
{
internal class DelegateCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public void Execute(object parameter)
{
_execute();
}
public bool CanExecute(object parameter)
{
if (_canExecute == null) return true;
return _canExecute();
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
internal class DelegateCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action<T> execute) : this(execute, null) { }
public DelegateCommand(Action<T> execute, Func<bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public bool CanExecute(object parameter)
{
if (_canExecute == null) return true;
return _canExecute();
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
应用MahApps.Metro框架
添加资源字典
添加资源字典到项目中,修改App.xaml
,在<Application.Resources>
中添加对应的资源字典
<Application x:Class="QiniuDiskManager.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:QiniuDiskManager"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Colors.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/Blue.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Accents/BaseLight.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
修改MainWindow界面
xaml文件中引用命名空间
xmlns:controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
MainWindow的根节点由Window
修改为controls:MetroWindow
<controls:MetroWindow x:Class="QiniuDiskManager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
mc:Ignorable="d"
Title="Qiniu Disk Manager" Height="450" Width="800" controls:TitleCaps="False" WindowStartupLocation="CenterScreen">
<!-- 右上角菜单按钮 -->
<controls:MetroWindow.RightWindowCommands>
<controls:WindowCommands x:Name="window_cmds">
<Button x:Name="btn_login_out" Content="退出账号" Padding="0 0 0 0" Cursor="Hand" Margin="5 0 5 0"/>
<TextBlock x:Name="txt_name" Text="用户:NULL" Margin="5 0 5 0" VerticalAlignment="Center" HorizontalAlignment="Center"></TextBlock>
</controls:WindowCommands>
</controls:MetroWindow.RightWindowCommands>
<Grid>
<controls:MetroAnimatedSingleRowTabControl x:Name="tab_shouye" Margin="0 0 0 0" TabStripPlacement="Top">
<controls:MetroTabItem Header="Files" Tag="-1" Padding="0" controls:ControlsHelper.HeaderFontSize="16">
<controls:MetroTabItem.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Margin="0 0 0 0" Width="{Binding}" Padding="10 5 5 5"></TextBlock>
</DataTemplate>
</controls:MetroTabItem.HeaderTemplate>
<Frame x:Name="frame_files" Source="./Pages/FilesPage.xaml" Padding="0" Margin="0" NavigationUIVisibility="Hidden" BorderThickness="0 1 0 0" BorderBrush="#1DADD7"></Frame>
</controls:MetroTabItem>
<controls:MetroTabItem Header="Downloads" Tag="-1" Padding="0" controls:ControlsHelper.HeaderFontSize="16">
<controls:MetroTabItem.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Margin="0 0 0 0" Width="{Binding}" Padding="5 5 5 5"></TextBlock>
</DataTemplate>
</controls:MetroTabItem.HeaderTemplate>
<Frame x:Name="frame_downloads" Source="./Pages/DownloadsPage.xaml" Padding="0" Margin="0" NavigationUIVisibility="Hidden" BorderThickness="0 1 0 0" BorderBrush="#1DADD7"></Frame>
</controls:MetroTabItem>
<controls:MetroTabItem Header="Config" Tag="-1" Padding="0" controls:ControlsHelper.HeaderFontSize="16">
<controls:MetroTabItem.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Margin="0 0 0 0" Width="{Binding}" Padding="5 5 10 5"></TextBlock>
</DataTemplate>
</controls:MetroTabItem.HeaderTemplate>
<Frame x:Name="frame_config" Source="./Pages/ConfigPage.xaml" Padding="0" Margin="0" NavigationUIVisibility="Hidden" BorderThickness="0 1 0 0" BorderBrush="#1DADD7"></Frame>
</controls:MetroTabItem>
</controls:MetroAnimatedSingleRowTabControl>
</Grid>
</controls:MetroWindow>
在Pages文件夹下新建页面
ConfigPage.xaml
DownloadsPage.xaml
FilesPage.xaml
<Page x:Class="QiniuDiskManager.Pages.ConfigPage"
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"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:vm="clr-namespace:QiniuDiskManager.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="Config">
<Page.DataContext>
<vm:ConfigPageModel />
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="txt_hello" Text="{Binding ContentText, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="5 0 0 0"></TextBlock>
<Button Grid.Row="1" Height="26" Margin="5 0 5 0" Content="修改">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding SayHello}" CommandParameter="{Binding ElementName=txt_hello}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Page>
<Page x:Class="QiniuDiskManager.Pages.DownloadsPage"
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"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:vm="clr-namespace:QiniuDiskManager.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="DownloadsPage">
<Page.DataContext>
<vm:DownloadsPageModel />
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="txt_hello" Text="{Binding ContentText, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="5 0 0 0"></TextBlock>
<Button Grid.Row="1" Height="26" Margin="5 0 5 0" Content="修改">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding SayHello}" CommandParameter="{Binding ElementName=txt_hello}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Page>
<Page x:Class="QiniuDiskManager.Pages.FilesPage"
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"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:vm="clr-namespace:QiniuDiskManager.ViewModels"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="FilesPage">
<Page.DataContext>
<vm:FilesPageModel />
</Page.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="30"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" x:Name="txt_hello" Text="{Binding ContentText, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="5 0 0 0"></TextBlock>
<Button Grid.Row="1" Height="26" Margin="5 0 5 0" Content="修改">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding SayHello}" CommandParameter="{Binding ElementName=txt_hello}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Page>
在ViewModels文件夹下新建页面模型
ConfigPageModel.cs
DownloadsPageModel.cs
FilesPageModel.cs
using QiniuDiskManager.Bases;
using QiniuDiskManager.Commands;
using System.Windows.Controls;
using System.Windows.Input;
namespace QiniuDiskManager.ViewModels
{
public class ConfigPageModel : ViewModelBase
{
public string ContentText { get; set; }
public ConfigPageModel()
{
ContentText = "Config Page";
}
public ICommand SayHello
{
get
{
return new DelegateCommand<TextBlock>((txt) =>
{
ContentText = "Hello " + ContentText;
});
}
}
}
}
using QiniuDiskManager.Bases;
using QiniuDiskManager.Commands;
using System.Windows.Controls;
using System.Windows.Input;
namespace QiniuDiskManager.ViewModels
{
public class DownloadsPageModel : ViewModelBase
{
public string ContentText { get; set; }
public DownloadsPageModel()
{
ContentText = "Downloads Page";
}
public ICommand SayHello
{
get
{
return new DelegateCommand<TextBlock>((txt) =>
{
ContentText = "Hello " + ContentText;
});
}
}
}
}
using QiniuDiskManager.Bases;
using QiniuDiskManager.Commands;
using System.Windows.Controls;
using System.Windows.Input;
namespace QiniuDiskManager.ViewModels
{
public class FilesPageModel : ViewModelBase
{
public string ContentText { get; set; }
public FilesPageModel()
{
ContentText = "Files Page";
}
public ICommand SayHello
{
get
{
return new DelegateCommand<TextBlock>((txt) =>
{
ContentText = "Hello " + ContentText;
});
}
}
}
}
运行效果
单击每个页面的“修改”
按钮,实现在原有字符串变量前添加“Hello ”
,通过Binding
绑定实现对应TextBlock
控件的Text
内容的通知更新,实现效果如下:
总结
现在前后台只需要约定好控件的绑定属性名称、命令名称及参数,然后各自写各自的代码,前端只需要修改xaml文件里的内容,后端只需要修改对应ViewModel的cs文件,最后通过版本控制器(SVN或Git)提交自己修改的代码文件,便可以直接运行。比如A提交了xaml文件,B提交了cs文件,然后C从SVN拉下A和B提交的代码后可以直接运行。