Blazor SfDropDownList 聯動式下拉選單的設計 Part 1 : 符合預期規劃結果
當我在進行 Xamarin.Forms App 專案開發教學的時候,這個需求是學員一定需要做的練習,畢竟,這樣的應用需求是個相當普遍的;在這篇文章中,將會說明如何在 Blazor 專案內,使用 Syncfusion 所提供的 SfDropDownList 元件,做出這樣的設計。
不過,在這篇文章中也會探討一些意外情況,也就是說,當使用其他方式來進行這樣需求的程式設計時候,會有意想不到的效果,也就是下拉選單的清單項目,無法正常顯示在螢幕上。
首先,先來看看這樣的需求,該如何來設計。
這篇文章的原始碼位於 bzDropDownListCorrelation
需求說明
在這個範例專案中,需要設計三個下拉選單,如上圖所示。
第一個下拉選單(在此稱為我的下拉選單)在程式啟動之後,將會有預設清單項目可以來選擇,而第二個(在此稱為你的下拉選單)下拉選單與第三個(在此稱為他的下拉選單)下拉選單則預設沒有任何項目可以選擇,必須要等到我的下拉選單選擇一個新的選單項目之後,底下的兩個下拉選單將會清空,並且會依據我的下拉選單所選擇的內容,建立其可以選擇的清單項目。
例如,在下圖中,在我的下拉選單中,選擇了 [我的選擇清單項目 8],一旦我的下拉選單項目有變動,此時,將會依據剛剛選擇的 [我的選擇清單項目 8] 項目,建立起你的下拉選單與他的下拉選單可以使用的選擇清單項目,這個時候,將會看到如同下圖的樣貌。
另外,在每個下拉選單控制項的下方,將會有兩個數值,分別代表這個下拉選單控制項目的資料來源 DataSource 這個屬性所綁定的物件內,已經有多少的項目存在,另外一個數值則表示這個下拉選單所選擇項目 Id 值為多少。
每個下拉選單項目的 Id 值將會從 1 開始編碼,而當每個下拉選單建立之後,將會產生一個項目,名稱為 [請選擇],而 Id 值為 -1,並且設定這個清單項目為預設的選擇項目。
Problem.razor 下拉選單元件設計程式碼
在這個範例中,所有的測試功能,都設計在 [Problem.razor] Razor 元件內,並且在 [Index.razor] 首頁元件內,加入這個 [Problem.razor] 元件。
對於 [Index.razor] 元件,其原始碼如下
@page "/"
<h1>Hello, SfDropDownList!</h1>
<Problem/>
<SurveyPrompt Title="How is Blazor working for you?" />
而 [Problem.razor] Razor 元件的程式碼如下
@using Syncfusion.Blazor.DropDowns
@using Syncfusion.Blazor.Buttons
@using bzDropDownListCorrelation.Data
@inject ProblemViewModel RazorModel
<SfButton @onclick="Click">OK</SfButton>
<div>
<SfDropDownList TValue="int" TItem="MyClass" Placeholder="我的條件"
Enabled="@RazorModel.ReadyGo"
DataSource="@RazorModel.MyClassList" @bind-Value="@RazorModel.QueryCondition.MyId">
<DropDownListEvents TValue="int" TItem="MyClass"
ValueChange="RazorModel.MasterListChanged" />
<DropDownListFieldSettings Text="Name" Value="Id" />
</SfDropDownList>
</div>
<div>
@RazorModel.MyClassList.Count()
</div>
<div>
@RazorModel.QueryCondition.MyId
</div>
<div>
<SfDropDownList TValue="int" TItem="YouClass" Placeholder="你的條件"
Enabled="@RazorModel.ReadyGo"
DataSource="@RazorModel.YourClassList" @bind-Value="@RazorModel.QueryCondition.YourId">
<DropDownListEvents TValue="int" TItem="YouClass" />
<DropDownListFieldSettings Text="Name" Value="Id" />
</SfDropDownList>
</div>
<div>
@RazorModel.YourClassList.Count()
</div>
<div>
@RazorModel.QueryCondition.YourId
</div>
<div>
<SfDropDownList TValue="int" TItem="YouClass" Placeholder="他的條件"
Enabled="@RazorModel.ReadyGo"
DataSource="@RazorModel.HisClassList" @bind-Value="@RazorModel.QueryCondition.HisId">
<DropDownListEvents TValue="int" TItem="YouClass" />
<DropDownListFieldSettings Text="Name" Value="Id" />
</SfDropDownList>
</div>
<div>
@RazorModel.HisClassList.Count()
</div>
<div>
@RazorModel.QueryCondition.HisId
</div>
@code {
protected async override Task OnInitializedAsync()
{
await RazorModel.ProblemViewModelInit();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender == true)
{
#region 若把下拉選單的初始化呼叫動作放在這裡,需要額外呼叫,否則會無法生效
//await RazorModel.ProblemViewModelInit();
//StateHasChanged();
#endregion
}
}
void Click() { }
}
在這個 [Problem.razor] Razor 元件內,最上方有三行 Using ,這裡是在宣告這裡類別將會使用到的命名空間(注意,每個 Razor 元件,在建置期間,都會由編譯器產生成為一個類別,該類別的名稱,就是該元件的名稱)。
而對於 @inject ProblemViewModel RazorModel
則是宣告在這個元件內,將會由相依性注入容器注入一個 [ProblemViewModel] 類別執行個體,也就是說,這裡採用的類似於 MVVM 的設計方式,將畫面內容與商業邏輯分隔開來,所有在畫面上要看到的內容、大小、顏色、位置等等,都會在 Razor 元件內 (也就是通稱的 View) 使用 HTML + CSS 來設計,而對於該頁面的商業邏輯程式碼,將會移到 ViewModel 內來使用 C# 程式碼來進行設計;與傳統的 MVVM 最大的差異,那就是在這裡對於 View 與 ViewModel 之間的互動,不是透過 NotifyPropertyChanged 這樣的機制來運行,而是透過了 Blazor 內建的 MVU 機制來做到。
對於 [ProblemViewModel] 這個類別,將會於 [Startup] 類別內的 [ConfigureServices] 方法內,使用 services.AddTransient<ProblemViewModel>();
敘述進行註冊到相依性容器內,這樣當要注入到任何類別或者元件內的時候,就會使用暫時生命週期的方式,注入到指定的類別內。
至於 [ProblemViewModel] 類別內容,等下會繼續說明
[Problem.razor] Razor 元件首先宣告一個 OK 按鈕,而對於其綁定的 [@onclick] 事件方法 void Click() { }
,似乎沒有做任何事情,那麼,為什麼需要在這裡放上一個這樣的按鈕呢?
對於這樣一個空白按鈕事件的設計,是為了要讓 Blazor 元件,可以觸發自動更新頁面的效果,怎麼說呢?在 Blazor 元件內,若想要手動觸發更新元件的效果時候,可以執行 [StateHasChanged()] 這個方法;而對於任何標籤 Element / Tag 有綁到 [@onclick] 這個事件的方法,只要觸發並且執行完成該事件的委派方法,則會自動地進行該元件更新的行為,就如同手動執行 [StateHasChanged()] 方法所得到相同效果。
因此,這樣的設計將會在等下會用到
接著,這裡宣告了三個 SfDropDownList ,分別代表 我的、你的、他的下拉選單控制項,每個控制項都透過了 [DataSource] 這個參數,指定該下拉選單的清單項目資料來源,而當使用者點選了該下拉選單,並且做出選擇之後,該遠則清單項目的 Id 值,將會儲存在 [RazorModel.QueryCondition] 類別內的某個屬性內。
使用 [我的下拉選單] 這個控制項來做說明,這個下拉選單的清單項目資料來源將會從 ViewModel , 也就是這個 ProblemViewModel 類別產生的執行個體, 物件內 [MyClassList] 這個集合清單屬性來獲取而得;當然,這個下拉選單內的清單項目想要顯示甚麼內容,就僅需要針對 [MyClassList] 這個集合性質物件來進行調整即可,對於綁定到的下拉選單控制項而言,就會顯示出相對應的最新清單項目內容。
在每個下拉選單控制項的下方,分別有這樣的宣告
<div>
@RazorModel.MyClassList.Count()
</div>
<div>
@RazorModel.QueryCondition.MyId
</div>
這也是為了除錯方便,可以從畫面上看到現在這個下拉選單內的 [DataSource] 所指定的集合物件究竟有多少物件存在、當使用者點選該下拉選單的時候,就必須一定要能夠看到這些數量的清單項目在螢幕上,否則,就是所設計的元件出了問題;而第二個則是顯示使用者當時點選的項目是哪一個,將會在這裡即時顯示出來。
在這個元件的最後面,將會使用到兩個元件生命週期的事件,分別是: [Task OnInitializedAsync()] 與 [Task OnAfterRenderAsync(bool firstRender)],在 [@code] 區段內的 C# 程式碼可以看的出來,對於 [ViewModel] 物件(也就是 [ProblemViewModel] 這個類別所注入的物件) 要進行相關資料初始化的時候,可以分別選擇這兩個事件來進行呼叫 [await RazorModel.ProblemViewModelInit()] 這個非同步方法,不過,在這裡會先使用 [Task OnInitializedAsync()] 這個生命週期事件來做為觸發與產生預設資料;當你使用的都是同步程式碼的時候(不含使用非同步的程式碼,但是使用封鎖式的等待作法),原則上不會遇到太多詭異的問題,但是若使用到非同步程式呼叫的的時候,許多詭異的現象就會發生了。
ProblemViewModel 的 ViewModel 程式碼
using bzDropDownListCorrelation.Data;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace bzDropDownListCorrelation
{
public class ProblemViewModel
{
public ProblemViewModel(MyClassService myClassService,
YourClassService youClassService)
{
MyClassService = myClassService;
YourClassService = youClassService;
}
public List<MyClass> MyClassList { get; set; } = new List<MyClass>();
public List<YouClass> YourClassList { get; set; } = new List<YouClass>();
public List<YouClass> HisClassList { get; set; } = new List<YouClass>();
public QueryCondition QueryCondition { get; set; } = new QueryCondition();
public MyClassService MyClassService { get; }
public YourClassService YourClassService { get; }
public bool ReadyGo { get; set; } = false;
public async Task ProblemViewModelInit()
{
var myLists = await MyClassService.Get();
#region 使用 AddRange + await 會造成資料無法讀取進來
//MyClassList.Clear();
//MyClassList.Insert(0, new MyClass { Name = "請選擇", Id = -1 });
//QueryCondition.MyId = -1;
//MyClassList.AddRange(await MyClassService.Get());
#endregion
MyClassList.Clear();
MyClassList = myLists;
MyClassList.Insert(0, new MyClass { Name = "請選擇", Id = -1 });
QueryCondition.MyId = -1;
YourClassList.Clear();
YourClassList.Insert(0, new YouClass { Name = "請選擇", Id = -1 });
QueryCondition.YourId = -1;
HisClassList.Clear();
HisClassList.Insert(0, new YouClass { Name = "請選擇", Id = -1 });
QueryCondition.HisId = -1;
ReadyGo = true;
}
public async Task MasterListChanged(Syncfusion.Blazor.DropDowns.ChangeEventArgs<int, MyClass> args)
{
if (QueryCondition.MyId <= 0) return;
var yourList = await YourClassService.Get("你的選擇清單項目", QueryCondition.MyId);
var hisList = await YourClassService.Get("他的選擇清單項目", QueryCondition.MyId);
#region 使用這個方式,會造成無法連動顯示
//YourClassList.Clear();
//YourClassList.Insert(0, new YouClass { Name = "請選擇", Id = -1 });
//QueryCondition.YourId = -1;
//YourClassList.AddRange(yourList);
//YourClassList = yourList;
#endregion
YourClassList.Clear();
YourClassList = yourList;
YourClassList.Insert(0, new YouClass { Name = "請選擇", Id = -1 });
QueryCondition.YourId = -1;
HisClassList.Clear();
HisClassList = hisList;
HisClassList.Insert(0, new YouClass { Name = "請選擇", Id = -1 });
QueryCondition.HisId = -1;
}
}
public class QueryCondition
{
public int MyId { get; set; }
public int YourId { get; set; }
public int HisId { get; set; }
}
}
在這個 [ProblemViewModel] 類別,將會透過相依性注入容器,注入到所要使用的 Razor 元件內;在建構函式內,將會注入 [MyClassService] 與 [YourClassService] 這兩個類別物件。
另外,在這個類別內宣告了許多屬性,這些屬性將會用於綁定到 Razor 元件上
該類別還有設計兩個方法
ProblemViewModelInit
在這個下拉選單元件建立的時候,會透過這個方法來建立起我的下拉選單可用的清單項目紀錄,在此是透過 [await MyClassService.Get()] 這個非同步方法呼叫的方式來取得;一旦取得了要顯示的所有顯示清單項目,便會將這個最新清單項目指派給要綁定屬性 [MyClassList],接著會在最前面置入一個額外選項,名稱為 "請選擇" 的選項,並且設定該選項為預設選擇項目。
之後,便會把你的下拉選單與他的下拉選單清空。
MasterListChanged
這是個 [SfDropDownList] 的 [ValueChange] 事件的委派事件,當該下拉選單的選擇項目有異動的時候,便會觸發這個 [MasterListChanged] 事件委派方法
在這個事件委派方法內,將會依據我的下拉選單的最新輸入的清單項目,動態產生出你的下拉選單與他的下拉選單之可用清單項目,並且把這兩個下拉選單之前的輸入結果,進行重新設定;不論是你的下拉選單或者是他的下拉選單的清單項目,都是透過非同步方法呼叫方式來取得。
MyClassService 程式碼
using bzDropDownListCorrelation.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace bzDropDownListCorrelation
{
public class MyClassService
{
public async Task<List<MyClass>> Get()
{
await Task.Delay(1000); // 模擬存取資料庫的非同步執行時間
List<MyClass> result = new();
for (int i = 1; i < 100; i++)
{
MyClass myClass = new()
{
Id = i,
Name=$"我的選擇清單項目 {i}",
};
result.Add(myClass);
}
return result;
}
}
}
底下為這個類別的程式碼
該類別僅設計一個非同步方法 [Get],會產生出 99 筆的清單項目,不過,在這裡使用了 await Task.Delay(1000)
敘述來按停 1 秒鐘的時間,代表模擬存取資料庫的非同步執行時間,因此,當網頁顯示在瀏覽器的時候,將會需要等候 1 秒的時間,我的下拉選單控制項,才可以看到這些清單項目可以使用。
YourClassService 程式碼
using bzDropDownListCorrelation.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace bzDropDownListCorrelation
{
public class YourClassService
{
public async Task<List<YouClass>> Get(string subject, int parentId)
{
Random random = new();
var maxItems = random.Next(10, 60);
var delay = random.Next(500, 3000);
await Task.Delay(delay); // 模擬存取資料庫的非同步執行時間
List<YouClass> result = new();
for (int i = 1; i < maxItems; i++)
{
YouClass myClass = new()
{
Id = i,
Name=$"{subject}{parentId} - {i}",
};
result.Add(myClass);
}
return result;
}
}
}
底下為這個類別的程式碼
該類別僅設計一個非同步方法 [Get],不過該方法需要提供兩個參數,第一個是該選擇清單項目要顯示的前導文字內容,第二個參數則是我的下拉選單所選擇清單項目 Id,這樣可以方便看出你的和他的下拉選單清單項目,會隨著我的下拉選單選擇結果而有我變化。接著會隨機產生出 10 - 60 筆的清單項目,不過,在這裡同樣的使用了 var delay = random.Next(500, 3000);await Task.Delay(delay);
敘述來按停 0.53 秒鐘的時間,代表模擬存取資料庫的非同步執行時間,因此,當我的下拉選單輸入項目有變動的時候,將會需要等候 0.53 秒的時間,你的下拉選單控制項與他的下拉選單控制項,才可以看到這些清單項目可以使用。
執行結果
請按下 F5 開始執行這個專案,嘗試進行操作,應該會如同當初所規劃的情境來運作
沒有留言:
張貼留言