主頁 > .NET開發 > [Windows] Prism 8.0 入門(下):Prism.Wpf 和 Prism.Unity

[Windows] Prism 8.0 入門(下):Prism.Wpf 和 Prism.Unity

2020-12-09 06:28:53 .NET開發

1. Prism.Wpf 和 Prism.Unity

這篇是 Prism 8.0 入門的第二篇文章,上一篇介紹了 Prism.Core,這篇文章主要介紹 Prism.Wpf 和 Prism.Unity,

以前做 WPF 和 Silverlight/Xamarin 專案的時候,我有時會把 ViewModel 和 View 放在不同的專案,ViewModel 使用 可移植類別庫專案,這樣 ViewModel 就與 UI 平臺無關,實作了代碼復用,這樣做還可以強制 View 和 ViewModel 解耦,

現在,即使在只寫 WPF 專案的情況下,但為了強制 ViewModel 和 View 假裝是陌生人,做到不留后路,我也傾向于把 View 和 ViewModel 放到不同專案,并且 ViewModel 使用 .Net Standard 作為目標框架,我還會假裝下個月 UWP 就要崛起了,我手頭的 WPF 專案中的 ViewModel 要做到平臺無關,方便我下個月把專案移植到 UWP 專案中,

但如果要使用 Prism 構建 MVVM 程式的話,上面這些根本不現實,首先,Prism 做不到平臺無關,它針對不同的平臺提供了不同的包,分別是:

  • 針對 WPF 的 Prism.Wpf
  • 針對 Xamarin Forms 的 Prism.Forms
  • 針對 Uno 平臺的 Prism.Uno

其次,根本就沒有針對 UWP 的 Prism.Windows(UWP 還有未來,忍住別哭),

所以,除非只使用 Prism.Core,否則要將 ViewModel 專案共享給多個平臺有點困難,畢竟用在 WPF 專案的 Prism.Wpf 本身就是個 Wpf 類別庫,

現在“撰寫平臺無關的 ViewModel 專案”這個話題就與 Prism 無關了,再把 Prism.Unity 和 Prism.Wpf 選為代表(畢竟這個組合比其它組合下載量多些),這篇文章就只用它們作為 Prism 入門的學習物件,

Prism.Core、Prism.Wpf 和 Prism.Unity 的依賴關系如上所示,其中 Prism.Core 實作了 MVVM 的核心功能,它是一個與平臺無關的專案,Prism.Wpf 里包含了 Dialog Service、Region、Module 和導航等幾個模塊,都是些用在 WPF 的功能,Prism.Unity 本身沒幾行代碼,它表示為 Prism.Wpf 選擇了 UnityContainer 作為 IOC 容器,(另外還有 Prism.DryIoc 可以選擇,但從下載量看 Prism.Unity 是主流,)

就算只學習 Prism.Wpf,可它的模塊很多,一篇文章實在塞不下,我選擇了 Dialog Service 作為代表,因為它的實作思想和其它的差不多,而且彈窗還是 WPF 最常見的操作,這篇文章將通過以下內容講解如何使用 Prism.Wpf 構建一個 WPF 程式:

  • PrismApplication
  • RegisterTypes
  • XAML ContainerProvider
  • ViewModelLocator
  • Dialog Service

Prism 的最新版本是 8.0.0.1909,由于 Prism.Unity 依賴 Prism.Wpf,所以只需安裝 Prism.Unity:

Install-Package Prism.Unity -Version 8.0.0.1909

2. PrismApplication

安裝好 Prism.Wpf 和 Prism.Unity 后,下一步要做的是將 App.xaml 的型別替換為 PrismApplication

<prism:PrismApplication x:
                        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:prism="http://prismlibrary.com/">
    <Application.Resources>
    </Application.Resources>
</prism:PrismApplication>

上面是修改過的 App.xaml,將 Application 改為 prism:PrismApplication,并且移除了 StartupUri="MainWindow.xaml"

接下來不要忘記修改 App.xaml.cs:

public partial class App : PrismApplication
{
    public App()
    {
    }

    protected override Window CreateShell()
        => Container.Resolve<ShellWindow>();
}

PrismApplication 不使用 StartupUri ,而是使用 CreateShell 方法創建主視窗,CreateShell 是必須實作的抽象函式,PrismApplication 提供了 Container 屬性,CreateShell 函式里通常使用 Container 創建主視窗,

3. RegisterTypes

其實在使用 CreateShell 函式前,首先必須實作另一個抽象函式 RegisterTypes,由于 Prism.Wpf 相當依賴于 IOC,所以要現在 PrismApplication 里注冊必須的型別或依賴,PrismApplication 里已經預先注冊了 DialogServiceEventAggregatorRegionManager 等必須的型別(在 RegisterRequiredTypes 函式里),其它型別可以在 RegisterTypes 里注冊,它看起來像這樣:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    // Core Services

    // App Services

    // Views
    containerRegistry.RegisterForNavigation<BlankPage, BlankViewModel>(PageKeys.Blank);
    containerRegistry.RegisterForNavigation<MainPage, MainViewModel>(PageKeys.Main);
    containerRegistry.RegisterForNavigation<ShellWindow, ShellViewModel>();

    // Configuration
    var configuration = BuildConfiguration();

    // Register configurations to IoC
    containerRegistry.RegisterInstance<IConfiguration>(configuration);
}

4. XAML ContainerProvider

在 XAML 中直接實體化 ViewModel 并設定 DataContext 是 View 和 ViewModel 之間建立關聯的最基本的方法:

<UserControl.DataContext>
    <viewmodels:MainViewModel/>
</UserControl.DataContext>

但現實中很難這樣做,因為相當一部分 ViewModel 都會在建構式中注入依賴,而 XAML 只能實體化具有無引數建構式的型別,為了解決這個問題,Prism 提供了 ContainerProvider 這個工具,通過設定 TypeName 從 Container 中決議請求的型別,它的用法如下:

<TextBlock
  Text="{Binding
    Path=Foo,
    Converter={prism:ContainerProvider {x:Type local:MyConverter}}}" />

<Window>
  <Window.DataContext>
    <prism:ContainerProvider Type="{x:Type local:MyViewModel}" />
  </Window.DataContext>
</Window>

5. ViewModelLocator

Prism 還提供了 ViewModelLocator,用于將 View 的 DataContext 設定為對應的 ViewModel:

<Window x:
    ...
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">

在將 View 的 ViewModelLocator.AutoWireViewModel 附加屬性設定為 True 的同時,Prism 會為查找這個 View 對應的 ViewModel 型別,然后從 Container 中決議這個型別并設定為 View 的 DataContext,它首先查找 ViewModelLocationProvider 中已經使用 Register 注冊的型別,Register 函式的使用方式如下:

ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();

如果型別未在 ViewModelLocationProvider 中注冊,則根據約定好的命名方式找到 ViewModel 的型別,這是默認的查找邏輯的原始碼:

var viewName = viewType.FullName;
viewName = viewName.Replace(".Views.", ".ViewModels.");
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
return Type.GetType(viewModelName);

例如 PrismTest.Views.MainView 這個類,對應的 ViewModel 型別就是 PrismTest.ViewModels.MainViewModel

當然很多專案都不符合這個命名規則,那么可以在 App.xaml.cs 中重寫 ConfigureViewModelLocator 并呼叫 ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver 改變這個查找規則:

protected override void ConfigureViewModelLocator()
{
    base.ConfigureViewModelLocator();

    ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
    {
        var viewName = viewType.FullName.Replace(".ViewModels.", ".CustomNamespace.");
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var viewModelName = $"{viewName}ViewModel, {viewAssemblyName}";
        return Type.GetType(viewModelName);
    });
}

6. Dialog Service

Prism 7 和 8 相對于以往的版本最大的改變在于 View 和 ViewModel 的互動,現在的處理方式變得更加易于使用,這篇文章以其中的 DialogService 作為代表講解 Prism 如何實作 View 和 ViewModel 之間的互動,

DialogService 內部會呼叫 ViewModelLocator.AutoWireViewModel,所以使用 DialogService 呼叫的 View 無需添加這個附加屬性,

以往在 WPF 中需要彈出一個視窗,首先新建一個 Window,然后呼叫 ShowDialogShowDialog 阻塞當前執行緒,直到彈出的 Window 關閉,這時候還可以拿到一個回傳值,具體代碼差不多是這樣:

var window = new CreateUserWindow { Owner = this };
var dialogResult = window.ShowDialog();
if (dialogResult == true)
{
    var user = window.User;
    //other code;
}

簡單直接有用,但在 MVVM 模式中,開發者要假裝自己不知道要呼叫的 View,甚至不知道要呼叫的 ViewModel,開發者只知道要執行的這個操作的名字,要傳什么引數,拿到什么結果,至于具體由誰去執行,開發者要假裝不知道(雖然很可能都是自己寫的),為了做到這種效果,Prism 提供了 IDialogService 介面,這個介面的具體實作已經在 PrismApplication 里注冊了,用戶通常只需要從建構式里注入這個服務:

public MainWindowViewModel(IDialogService dialogService)
{
    _dialogService = dialogService;
}

IDialogService 提供兩組函式,分別是 ShowShowDialog,對應非模態和模態視窗,它們的引數都一樣:彈出的對話框的名稱、傳入的引數、對話框關閉時呼叫的回呼函式:

void ShowDialog(string name, IDialogParameters parameters, Action<IDialogResult> callback);

其中 IDialogResult 型別包含 ButtonResult 型別的 Result 屬性和 IDialogParameters 型別的 Parameters 屬性,前者用于標識關閉對話框的動作(Yes、No、Cancel等),后者可以傳入任何型別的引數作為具體的回傳結果,下面代碼展示了一個基本的 ShowDialog 函式呼叫方式:

var parameters = new DialogParameters
{
    { "UserName", "Admin" }
};

_dialogService.ShowDialog("CreateUser", parameters, dialogResult =>
{
    if (dialogResult.Result == ButtonResult.OK)
    {
        var user = dialogResult.Parameters.GetValue<User>("User");
        //other code
    }
});

為了讓 IDialogService 知道上面代碼中 “CreateUser” 對應的 View,需要在 'App,xaml.cs' 中的 RegisterTypes 函式中注冊它對應的 Dialog:

containerRegistry.RegisterDialog<CreateUserView>("CreateUser");

上面這種注冊方式需要依賴 ViewModelLocator 找到對應的 ViewModel,也可以直接注冊 View 和對應的 ViewModel:

containerRegistry.RegisterDialog<CreateUserView, CreateUserViewModel>("CreateUser");

有沒有發現上面的 CreateUserWindow 變成了 CreateUserView?因為使用 DialogService 的時候,View 必須是一個 UserControl,DialogService 自己創建一個 Window 將 View 放進去,這樣做的好處是 View 可以不清楚自己是一個彈框或者導航的頁面,或者要用在擁有不同 Window 樣式的其它專案中,反正只要實作邏輯就好了,由于 View 是一個 UserControl,它不能直接控制擁有它的 Window,只能通過在 View 中添加附加屬性定義 Window 的樣式:

<prism:Dialog.WindowStyle>
    <Style TargetType="Window">
        <Setter Property="prism:Dialog.WindowStartupLocation" Value="https://www.cnblogs.com/dino623/p/CenterScreen" />
        <Setter Property="ResizeMode" Value="https://www.cnblogs.com/dino623/p/NoResize"/>
        <Setter Property="ShowInTaskbar" Value="https://www.cnblogs.com/dino623/p/False"/>
        <Setter Property="SizeToContent" Value="https://www.cnblogs.com/dino623/p/WidthAndHeight"/>
    </Style>
</prism:Dialog.WindowStyle>

最后一步是實作 ViewModel,對話框的 ViewModel 必須實作 IDialogAware 介面,它的定義如下:

public interface IDialogAware
{
    /// <summary>
    /// 確定是否可以關閉對話框,
    /// </summary>
    bool CanCloseDialog();

    /// <summary>
    /// 關閉對話框時呼叫,
    /// </summary>
    void OnDialogClosed();

    /// <summary>
    /// 在對話框打開時呼叫,
    /// </summary>
    void OnDialogOpened(IDialogParameters parameters);

    /// <summary>
    /// 將顯示在視窗標題欄中的對話框的標題,
    /// </summary>
    string Title { get; }

    /// <summary>
    /// 指示 IDialogWindow 關閉對話框,
    /// </summary>
    event Action<IDialogResult> RequestClose;
}

一個簡單的實作如下:

public class CreateUserViewModel : BindableBase, IDialogAware
{
    public string Title => "Create User";

    public event Action<IDialogResult> RequestClose;

    private DelegateCommand _createCommand;
    public DelegateCommand CreateCommand => _createCommand ??= new DelegateCommand(Create);

    private string _userName;
    public string UserName
    {
        get { return _userName; }
        set { SetProperty(ref _userName, value); }
    }
   
    public virtual void RaiseRequestClose(IDialogResult dialogResult)
    {
        RequestClose?.Invoke(dialogResult);
    }

    public virtual bool CanCloseDialog()
    {
        return true;
    }

    public virtual void OnDialogClosed()
    {

    }

    public virtual void OnDialogOpened(IDialogParameters parameters)
    {
        UserName = parameters.GetValue<string>("UserName");
    }

    protected virtual void Create()
    {
        var parameters = new DialogParameters
        {
            { "User", new User{Name=UserName} }
        };

        RaiseRequestClose(new DialogResult(ButtonResult.OK, parameters));
    }
}

上面的代碼在 OnDialogOpened 中讀取傳入的引數,在 RaiseRequestClose 關閉對話框并傳遞結果,至此就完成了彈出對話框并獲取結果的整個流程,

自定義 Window 樣式在 WPF 程式中很流行,DialogService 也支持自定義 Window 樣式,假設 MyWindow 是一個自定義樣式的 Window,自定義一個繼承它的 MyPrismWindow 型別,并實作介面 IDialogWindow

public partial class MyPrismWindow: MyWindow, IDialogWindow
{
    public IDialogResult Result { get; set; }
}

然后呼叫 RegisterDialogWindow 注冊這個 Window 型別,

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterDialogWindow<MyPrismWindow>();
}

這樣 DialogService 將會使用這個自定義的 Window 型別作為 View 的視窗,

7. 結語

這篇文章介紹了如何使用 Prism.Wpf 創建一個 WPF 程式,雖然只介紹了 IDialogService,但其它模塊也大同小異,為了讓這篇文章盡量簡短我舍棄了它們的說明,

如果討厭 Prism.Wpf 的臃腫,或者需要創建面向多個 UI 平臺的專案,也可以只使用輕量的 Prism.Core,

如果已經厭倦了 Prism,可以試試即將發布的 MVVM Toolkit,它基本就是個 MVVM Light 的性能加強版,而且也更時髦,

8. 參考

https://github.com/PrismLibrary/Prism

https://prismlibrary.com/docs/index.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/231691.html

標籤:WPF

上一篇:C#高性能動態獲取物件屬性值

下一篇:WPF 開發的實用小工具(附原始碼)持續更新(六)嵌入桌面

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more