查殺程式小工具——WPF和MVVM初體驗

zer0black發表於2020-10-09

最近因為工作需要,研究了一下桌面應用程式。在winform、WPF、Electron等幾種技術裡,最終選擇了WPF作為最後的選型。WPF最吸引我的地方,就是MVVM模式了。MVVM模式完全把介面和業務剝離開來,頁面所有操作都通過資料來驅動。更替頁面不用修改業務程式碼邏輯。

以一個查殺程式的小工具來作為初次學習的成果總結。日常開發Java Web程式的時候,程式遇到埠占用問題,通過命令查詢埠、查詢程式、殺死程式,這一套命令敲下來過於麻煩。於是就寫了這麼一個小Demo,即作為學習使用,也為以後工作降低工作量。

需求設計

  1. 程式列表:展示所有經常的列表,按照應用名稱正序排序。列表展示程式名、PID、協議、本機IP:埠、遠端IP:埠、程式路徑
  2. 搜尋框:進行埠搜尋,在經常列表中展示搜尋結果
  3. 重新整理按鈕:重新整理程式列表
  4. 殺死按鈕:選中程式,進行程式的殺死。殺死程式後重新整理程式列表

關鍵要點

  1. DataContext
    DataContext主要作用是用於繫結資料來源,預設值為null。
    DataContext是FrameworkElement中的一個屬性。而絕大部分的UI元件都繼承路徑中都有FrameworkElement類,所以我們可以認為,大部分UI元件都有DataContext屬性。並且設定了某個物件的DataContext,那麼會對這個物件的所有子物件都會產生同樣的影響。
    所以一般來說,我們都會在頂級物件(Window物件)中去設定DataContext屬性。

  2. 使用MVVM的意義
    使用統一開發模式最大的優點,是統一團隊的思維方式和實現方式,從思維上保持程式碼的整潔。每個理解了模式的人都知道程式碼該怎麼寫。此外,MVVM模式在架構上解耦的比較徹底,資料驅動介面的模式也可讓結構更清晰。由於業務和介面剝離,業務程式碼的可測性、可讀性、可替換性得到提升。所以,既然WPF支援MVVM模式,就不要把WPF寫成WinForm。

  3. View 和 ViewModel
    View是UI、ViewModel是介面的資料模型。ViewModel和View是怎麼溝通的呢?ViewModel只會給View傳遞兩種資料:屬性資料和運算元據。傳遞資料用一般的資料模型來處理,傳遞操作用命令屬性來處理。

專案結構

引用包說明

  1. MaterialDesignThemes:主要用於介面的美化,通過NuGet包管理搜尋MaterialDesignThemes直接安裝
  2. 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>

相關文章