最近因為工作需要,研究了一下桌面應用程式。在winform、WPF、Electron等幾種技術裡,最終選擇了WPF作為最後的選型。WPF最吸引我的地方,就是MVVM模式了。MVVM模式完全把介面和業務剝離開來,頁面所有操作都通過資料來驅動。更替頁面不用修改業務程式碼邏輯。
以一個查殺程式的小工具來作為初次學習的成果總結。日常開發Java Web程式的時候,程式遇到埠占用問題,通過命令查詢埠、查詢程式、殺死程式,這一套命令敲下來過於麻煩。於是就寫了這麼一個小Demo,即作為學習使用,也為以後工作降低工作量。
需求設計
- 程式列表:展示所有經常的列表,按照應用名稱正序排序。列表展示程式名、PID、協議、本機IP:埠、遠端IP:埠、程式路徑
- 搜尋框:進行埠搜尋,在經常列表中展示搜尋結果
- 重新整理按鈕:重新整理程式列表
- 殺死按鈕:選中程式,進行程式的殺死。殺死程式後重新整理程式列表
關鍵要點
-
DataContext
DataContext主要作用是用於繫結資料來源,預設值為null。
DataContext是FrameworkElement中的一個屬性。而絕大部分的UI元件都繼承路徑中都有FrameworkElement類,所以我們可以認為,大部分UI元件都有DataContext屬性。並且設定了某個物件的DataContext,那麼會對這個物件的所有子物件都會產生同樣的影響。
所以一般來說,我們都會在頂級物件(Window物件)中去設定DataContext屬性。 -
使用MVVM的意義
使用統一開發模式最大的優點,是統一團隊的思維方式和實現方式,從思維上保持程式碼的整潔。每個理解了模式的人都知道程式碼該怎麼寫。此外,MVVM模式在架構上解耦的比較徹底,資料驅動介面的模式也可讓結構更清晰。由於業務和介面剝離,業務程式碼的可測性、可讀性、可替換性得到提升。所以,既然WPF支援MVVM模式,就不要把WPF寫成WinForm。 -
View 和 ViewModel
View是UI、ViewModel是介面的資料模型。ViewModel和View是怎麼溝通的呢?ViewModel只會給View傳遞兩種資料:屬性資料和運算元據。傳遞資料用一般的資料模型來處理,傳遞操作用命令屬性來處理。
專案結構
引用包說明
- MaterialDesignThemes:主要用於介面的美化,通過NuGet包管理搜尋MaterialDesignThemes直接安裝
- Prism.Wpf:是實現MVVM的框架,通過NuGet包管理搜尋Prism.Wpf直接安裝
專案目錄結構說明
WinPidKiller 專案名
- Models 業務資料模型層
NetworkInfo.cs 網路埠資料模型
ProcessInfo.cs 程式資料模型
- Services 業務邏輯層
IProcessInfoService.cs 程式業務操作介面
- impl 業務邏輯實現
ProcessInfoService.cs 程式業務操作實現類
- ViewModels 檢視資料模型層,溝通View和Model的重要元件
ProcessItemViewModel.cs 單行程式檢視資料模型(列表中每行資料的模型)
MainWindowViewModel.cs 主檢視資料模型
- Views 介面層
MainWindow.xmal 主視窗檔案
程式碼解釋說明
Models
資料模型僅針對於業務資料
NetworkInfo.cs
namespace WinPidKiller.Models
{
class NetworkInfo
{
public string Pid { get; set; }
public string AgreeMent { get; set; }
public string LocalIp { get; set; }
public string RemoteIp { get; set; }
}
}
ProcessInfo.cs
namespace WinPidKiller.Models
{
class ProcessInfo
{
public string Name { get; set; }
public string Pid { get; set; }
public string AgreeMent { get; set; }
public string LocalIp { get; set; }
public string RemoteIp { get; set; }
}
}
Services
僅包含ProcessInfoService類,主要實現埠的查詢(通過呼叫cmd程式),程式的獲取和殺死等操作
ProcessInfoService.cs
namespace WinPidKiller.Services.Impl
{
class ProcessInfoService : IProcessInfoService
{
/**
* 若port為空則獲取所有程式資訊
* 若port不為空則獲取佔用port的執行緒
*/
public List<ProcessInfo> GetAllProcessInfo(String port)
{
List<ProcessInfo> processInfoList = new List<ProcessInfo>();
// 拿到所有程式
Dictionary<int, Process> processMap = GetAllProcess();
List<NetworkInfo> networkInfos = null;
if (!(string.IsNullOrEmpty(port)))
{
// 根據port查詢出對應的埠資訊,展示對應程式資訊
networkInfos = GetPortInfo(port);
} else
{
networkInfos = GetPortInfo();
}
foreach (NetworkInfo networkInfo in networkInfos)
{
ProcessInfo processInfo = new ProcessInfo();
int.TryParse(networkInfo.Pid, out int pid);
Process process = processMap[pid];
processInfo.Name = process.ProcessName;
processInfo.Pid = process.Id.ToString();
processInfo.AgreeMent = networkInfo.AgreeMent;
processInfo.LocalIp = networkInfo.LocalIp;
processInfo.RemoteIp = networkInfo.RemoteIp;
processInfoList.Add(processInfo);
}
return processInfoList;
}
/**
* 獲取所有程式資訊
*/
public List<ProcessInfo> GetAllProcessInfo()
{
return GetAllProcessInfo(null);
}
/**
* 根據pid列表殺死所有程式
*/
public void KillProcess(List<string> pidList)
{
if (pidList == null || pidList.Count == 0)
{
MessageBox.Show("請選擇正確的程式號");
return;
}
Dictionary<int, Process> processMap = GetAllProcess();
StringBuilder sb = new StringBuilder();
foreach (var pidStr in pidList)
{
int.TryParse(pidStr, out int pid);
Process process = processMap[pid];
try
{
process.Kill();
sb.Append("已殺掉");
sb.Append(process.ProcessName);
sb.Append("程式!!!");
}
catch (Win32Exception e)
{
sb.Append(process.ProcessName);
sb.Append(e.Message.ToString());
}
catch (InvalidOperationException e)
{
sb.Append(process.ProcessName);
sb.Append(e.Message.ToString());
}
}
MessageBox.Show(sb.ToString());
}
/**
* 獲取所有原始程式資訊,並封裝為Dictionary
*/
private Dictionary<int, Process> GetAllProcess()
{
Process[] processes = Process.GetProcesses();
return processes.ToDictionary(key => key.Id, process => process);
}
/**
* 獲取所有埠資訊
*/
private List<NetworkInfo> GetPortInfo()
{
return GetPortInfo(null);
}
/**
* 通過埠取出所有相關的資料
*/
private List<NetworkInfo> GetPortInfo(string port)
{
List<NetworkInfo> networkInfoList = new List<NetworkInfo>();
Process process = CreateCmd();
process.Start();
if (string.IsNullOrEmpty(port))
{
process.StandardInput.WriteLine(string.Format("netstat -ano"));
} else
{
process.StandardInput.WriteLine(string.Format("netstat -ano|find \"{0}\"", port));
}
process.StandardInput.WriteLine("exit");
StreamReader reader = process.StandardOutput;
string strLine = reader.ReadLine();
while (!reader.EndOfStream)
{
strLine = strLine.Trim();
if (strLine.Length > 0 && ((strLine.Contains("TCP") || strLine.Contains("UDP"))))
{
Regex r = new Regex(@"\s+");
string[] strArr = r.Split(strLine);
// 解析資料格式為 TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 692
int defaultResultLength = 5;
if (strArr.Length == defaultResultLength)
{
NetworkInfo networkInfo = new NetworkInfo();
// 只拿第一行資料,拿完就撤(每個PID展示一個port就行)
networkInfo.AgreeMent = strArr[0];
networkInfo.LocalIp = strArr[1];
networkInfo.RemoteIp = strArr[2];
networkInfo.Pid = strArr[4];
networkInfoList.Add(networkInfo);
}
}
strLine = reader.ReadLine();
}
reader.Close();
process.Close();
return networkInfoList;
}
/**
* 建立cmd控制元件
*/
private Process CreateCmd()
{
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
return process;
}
}
}
ViewModels
主要實現程式列表中單個程式的資料模型ProcessItemViewModel的實現,ProcessItemViewModel比業務資料模型多了選中屬性selectItem。另外包含主窗體模型,完成剩下的資料和命令傳遞。
ProcessItemViewModel.cs
namespace WinPidKiller.ViewModels
{
class ProcessItemViewModel : BindableBase
{
public ProcessInfo ProcessInfo { get; set; }
private Boolean selectItem;
public Boolean SelectItem
{
get { return selectItem; }
set
{
selectItem = value;
SetProperty(ref selectItem, value);
}
}
}
}
MainWindowViewModel.cs
namespace WinPidKiller.ViewModels
{
/**
* 做雙向繫結,port提供查詢框用,processInfo列表提供dataGrid用
*/
class MainWindowViewModel : BindableBase
{
private int port;
public int Port
{
get { return port; }
set {
port = value;
SetProperty(ref port, value);
}
}
/**
* 如果這個DataList列表的內容需要同步重新整理,
* 則型別必須是ObservableCollection。
* 否則就算控制元件與資料繫結成功,控制元件只在初始化時能夠正確顯示資料,
* 之後資料發生改變時,控制元件不會自動重新整理。
*/
private ObservableCollection<ProcessItemViewModel> processItemList;
public ObservableCollection<ProcessItemViewModel> ProcessItemList
{
get { return processItemList; }
set {
processItemList = value;
SetProperty(ref processItemList, value);
}
}
public MainWindowViewModel()
{
// 載入資料
LoadProcessInfo();
QueryPortCommand = new DelegateCommand(new Action(QueryPortCommandExec));
KillCommand = new DelegateCommand(new Action(KillCommandExec));
RefreshCommand = new DelegateCommand(new Action(RefreshCommandExec));
}
private void LoadProcessInfo()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList = new ObservableCollection<ProcessItemViewModel>();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo()));
}
// 繫結檢索命令 和 kill命令
public DelegateCommand QueryPortCommand { get; set; }
public DelegateCommand KillCommand { get; set; }
public DelegateCommand RefreshCommand { get; set; }
private void QueryPortCommandExec()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList.Clear();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo(port.ToString())));
}
private void RefreshCommandExec()
{
IProcessInfoService processInfoService = new ProcessInfoService();
processItemList.Clear();
processItemList.AddRange(GetProcessItemViewModel(processInfoService.GetAllProcessInfo()));
}
private void KillCommandExec()
{
List<String> pidList = new List<string>();
foreach (var processItem in processItemList)
{
if (processItem.SelectItem)
{
pidList.Add(processItem.ProcessInfo.Pid);
}
}
IProcessInfoService processInfoService = new ProcessInfoService();
processInfoService.KillProcess(pidList);
// 殺死程式後,重新載入列表
this.QueryPortCommandExec();
}
/**
* 將ProcessInfo列表轉為ProcessItemViewModel列表
*/
private List<ProcessItemViewModel> GetProcessItemViewModel(List<ProcessInfo> processInfos)
{
List<ProcessItemViewModel> itemList = new List<ProcessItemViewModel>();
foreach(ProcessInfo processInfo in processInfos){
ProcessItemViewModel item = new ProcessItemViewModel() { ProcessInfo = processInfo };
itemList.Add(item);
}
return itemList;
}
}
}
主窗體介面
MainWindow.xaml.cs
namespace WinPidKiller
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
}
}
MainWindow.xaml
<Window x:Class="WinPidKiller.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:local="clr-namespace:WinPidKiller"
mc:Ignorable="d"
Title="Pid Killer" Height="450" Width="800"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<materialDesign:Card Grid.Row="0" Padding="8" Margin="8,5,8,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="110"></ColumnDefinition>
<ColumnDefinition Width="110"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=Port}" HorizontalAlignment="Stretch" Margin="0,0,110,0" FontSize="20" VerticalAlignment="Center"/>
<Button Content="檢索" Grid.Column="0" Width="100" HorizontalAlignment="Right" Command="{Binding QueryPortCommand}"/>
<Button Content="重新整理" Grid.Column="1" Width="100" HorizontalAlignment="Right" Command="{Binding RefreshCommand}"/>
<Button Content="殺死" Grid.Column="2" Width="100" HorizontalAlignment="Right" Command="{Binding KillCommand}"/>
</Grid>
</materialDesign:Card>
<materialDesign:Card Grid.Row="1" Padding="8" Margin="8,5,8,5" >
<DataGrid
x:Name="dataGrid"
FontSize="15"
AlternationCount="2"
GridLinesVisibility="Vertical"
AutoGenerateColumns="False"
IsReadOnly="True"
ItemsSource="{Binding Path=ProcessItemList}"
>
<DataGrid.Columns>
<DataGridCheckBoxColumn Width="50" Header="" Binding="{Binding Path=SelectItem,UpdateSourceTrigger=PropertyChanged}" IsReadOnly="False" CanUserSort="False" />
<DataGridTextColumn Width="Auto" Header="程式名" Binding="{Binding Path=ProcessInfo.Name}"/>
<DataGridTextColumn Width="100" Header="PID" Binding="{Binding Path=ProcessInfo.Pid}"/>
<DataGridTextColumn Width="80" Header="協議" Binding="{Binding Path=ProcessInfo.AgreeMent}"/>
<DataGridTextColumn Width="200" Header="本機IP:埠" Binding="{Binding Path=ProcessInfo.LocalIp}"/>
<DataGridTextColumn Width="200" Header="遠端IP:埠" Binding="{Binding Path=ProcessInfo.RemoteIp}"/>
</DataGrid.Columns>
</DataGrid>
</materialDesign:Card>
</Grid>
</Window>