[IT] 拯救 react

安裝個離線版 npm i - g create-react-app-offline 改用 crao 初始化 crao -n my-app 如果 node_modules 沒有安裝正常 rm -rf node_modules 重裝 node_modules npm install 重啟 react npm start

<span title='2024-08-16 01:44:44 +0800 +0800'>August 16, 2024</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] React + .Net

React + .Net 一、環境設置 Setup 1. 行前準備 Prerequisites 安裝 Node.js 和 npm node -v npm -v 安裝 .NET SDK dotnet --version 用 vs code 下載 ES7+ React/Redux/React-Native snippets 因為 rafce 很好用, 相當於, typescript 則是 tsrafce import React from 'react' const index = () => { return ( <div>index</div> ) } export default index 2. 創建 React 安裝 react npm install create-react-app 創建新的 react app (typescript) --template typescript 可指定使用 typescript npx create-react-app {project} --template typescript cd {project} 必要時可能要初始化一個新的 react 項目,確保版本是匹配的。 yarnpkg add --exact react-dom react-scripts 3. 基本結構 函數式元件(Functional Component): const Card: React.FC<Props> = ({ companyName, ticker, price }: Props): JSX.Element => { ... } Card 是一個函數式元件,使用 TypeScript 和 React。 React.FC<Props> 指定這個元件是接受 Props 作為參數的 React 函數式元件 ({ companyName, ticker, price }) 是從 Props 解構的屬性,這些屬性將會被傳入元件 JSX 標籤: return ( <div className='card'>...</div> ) 這是元件返回的 JSX,它描述了元件應該如何渲染。 4. State import React, { useState } from 'react' type Props = {} const Button: React.FC<Props> = (props: Props): JSX.Element => { const [count, setCount] = useState<number>(0); const onClick = (e: any) => { setCount(count + 1); console.log(e); } return ( <div> <button onClick={(e) => onClick(e)}>Click me</button> <p>You clicked {count} times</p> </div> ) } export default Button 引入 React 和 useState Hook: import React, { useState } from 'react' 從 React 包中引入 useState Hook,用於函數式元件中添加狀態。 使用 useState Hook 定義狀態 const [count, setCount] = useState<number>(0); 定義一個名為 count 的狀態變量,初始值為 0。 setCount 是用來更新 count 的函數。 useState<number>(0) 指定 count 的類型為 number。 定義 onClick 事件處理函數: const onClick = (e: any) => { setCount(count + 1); console.log(e); } onClick 是一個事件處理函數,接受一個事件參數 e。 每當按鈕被點擊時, count 會加 1,並且會在控制台輸出事件 e。 any 也可被寫成 MounthEvent 或 SyntheticEvent。

<span title='2024-06-11 22:00:23 +0800 +0800'>June 11, 2024</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;Rain Hu

[IT] ApiController Atrribute

ApiController Atrribute 當我在 API 專案中建立新的 controller 時,它會自帶一個 [ApiController] 屬性的控制器類別,而這個標籤的作用為何呢? [ApiController] public class TestController : ControllerBase { } 1. 自動 HTTP 400 回應 它會自動產生一個行為過濾器(action filter),當 ModelState.IsValid 為 false 時,自動回傳 400 Response。 2. 綁定來源參數推斷 可以更改模型綁定的約定,例如,[FromBody] 是針對複雜類型參數推斷的。 3. Multi/form-data 請求推理 對於標示 [FromForm] 的參數,推斷 Content-Type 為 multipart/form-data。 4. 屬性路由要求 強制要求所有操作都必須通過屬性路由。 ...

<span title='2024-06-09 20:27:03 +0800 +0800'>June 9, 2024</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] HTML + CSS

可搭配程式碼服用: https://github.com/intervalrain/htmls/tree/main/tutorial 使用者文檔: W3C 官網 MDN W3School 一、HTML 全名:超文本標記語言(HyperText Markup Language, HTML) 現最常使用的是 HTML5,由 W3C & WHATWG 所制定。 將檔案設置為 .html 即可建立一個 html 檔案。 二、HTML 標籤 標籤又稱元素,是 HTML 的基本組成單位。 標籤分為: 雙標籤和單標籤(絕大多數都是雙標籤)。 標籤不區分大小寫,但推薦小寫,因為小寫更規範 雙標籤,<標籤名>標籤體</標籤名> <marquee>Hello World</marquee> 單標籤,<標籤名/>,/可省略 <input/> 標籤之間的關係:並列關係、嵌套關係,可以使用 tab 鍵進行縮進。 <marquee> Hello World <input> </marquee> 三、HTML 標籤屬性 用於給標籤提供附加訊息。 可以寫在: 起始標籤或單標籤中,<標籤名 屬性名="屬性值" 屬性名="屬性值"> <marquee loop="1" bgcolor="orange">Hello World</marquee> <input type="password"> 有些特殊的屬性,沒有屬性名,只有屬性值: <input disabled> ...

<span title='2024-06-09 02:10:53 +0800 +0800'>June 9, 2024</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] EntityFramework Commands

前置作業 需安裝以下 Packages Microsoft.EntityFrameworkCore Microsoft.entityFrameworkCore.Design Microsoft.EntityFrameworkCore.Tools 設置好 []DbContext 範例 public class AppDbContext : DbContext { public DbSet<Reminder> Reminders { get; set; } = null!; public DbSet<User> Users { get; set; } = null!; public AppDbContext() { } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseNpgsql("Host=localhost;Port=5432;Username=********;Password=********;Database=testdb"); } } ...

<span title='2024-03-31 14:37:44 +0800 +0800'>March 31, 2024</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;Rain Hu

[IT] 用 C# 建置 Clean Architecture 專案

Configure C# Solution step by step 開啟 Terminal 新增解決方案 dontet new sln -o MySln 移至方案目錄 cd MySln 根據解決方案 hierarchy 建構專案 dotnet new webapi -o MySln.Api dotnet new classlib -o MySln.Application dotnet new classlib -o MySln.Domain dotnet new classlib -o MySln.Infrastructure dotnet new classlib -o MySln.Contracts 將所有專案加進解決方案 for proj in $(ls -r **/*.csproj) dotnet sln add $proj 建立專案之間的 dependency dotnet add MySln.Api reference MySln.Application MySln.Infrastructure MySln.Contracts dotnet add MySln.Infrastructure reference MySln.Application dotnet add MySln.Application reference MySln.Domain

<span title='2024-03-09 13:59:22 +0800 +0800'>March 9, 2024</span>&nbsp;·&nbsp;1 min&nbsp;·&nbsp;Rain Hu

[IT] 使用 Clean Architecture + DDD 建置 Restful API

程式碼 https://github.com/intervalrain/webapi_ca/ 正文 建置 Solution 首先先參考 Clean Architecture 最經典的同心圓,來確定我們需要將我們的解決方案做哪些分層: 我將使用 Restful API 做為我們 I/O (Presentation Layer) 並且我需要配備身份驗證的機制 (Presentation Layer) 我使用 PostgresDB 作為我的 (Infrastructure Layer) 我的核心商業邏輯 (Application / Domain Layer) 創建專案 dotnet new sln -o Mysln 進入專案所在的資料夾 cd Mysln 根據預先的分層建立專案資料夾,並且使用 dotnet 指令建立相對應的專案類型。 Api –> WebAPI Infrastructure –> classlib Contracts –> classlib Application –> classlib Domain –> classlib dotnet new webapi -o Mysln.Api dotnet new classlib -o Mysln.Contracts dotnet new classlib -o Mysln.Infrastructure dotnet new classlib -o Mysln.Application dotnet new classlib -o Mysln.Domain 接著我們需要把產生的專案資料夾,加入到我們的 Solution。 dotnet sln add Mysln.Api dotnet sln add Mysln.Application dotnet sln add Mysln.Contracts dotnet sln add Mysln.Domain dotnet sln add Mysln.Infrastructure 接下來按照 Clean Architecture 的依賴原則來設定 dependency,依我的專案來說依賴方向如下。 graph TD; Api-->Contracts; Api-->Application; Infrastructure-->Application Application-->Domain Api-.->Infrastructure dotnet add Mysln.Api reference Mysln.Contracts Mysln.Application dotnet add Mysln.Infrastructure reference Mysln.Application dotnet add Mysln.Application reference Mysln.Domain dotnet add Mysln.Api reference Mysln.Infrastructure 至此,已經完成了基本的 hierarchy 建置,接下來要為 Restful Client 做準備。 Login Authentication 作為驗證的需要,我們需要以下三種驗證檔案,包含兩個 Request 與一個 Response public record RegisterRequest( string FirstName, string LastName, string Email, string Password ); public record LoginRequest( string Email, string Password ); public record AuthenticationResponse( Guid Id, string FirstName, string LastName, string Email, string token ); 到 Controller 去設置註冊與登入的兩個路由,並且將之後的服務介面預先注入到其中。 [ApiController] [Route("auth")] public class AuthenticationController : ControllerBase { [HttpPost("register")] public IActionResult Register(RegisterRequest request) { return Ok(request); } [HttpPost("login")] public IActionResult Login(LoginRequest request) { return Ok(request); } } 接著我們創建 Application 中的服務,注意到因為 Application 不依賴於 Contracts,故我們這邊需要創建自己的 DataModel public record AuthenticationResult ( Guid Id, string FirstName, string LastName, string Email, string Token ); 接著我們定義出 Application 的 Service。 public interface IAuthenticationService { AuthenticationResult Register(string firstName, string lastName, string email, string password); AuthenticationResult Login(string email, string password); } 定義好我們的 service interface 之後,就可以到 Presentation 中將我們的 service 注入到 presentation 之中。 [ApiController] [Route("auth")] public class AuthenticationController : ControllerBase { private readonly IAuthenticationService _authenticationService; public AuthenticationController(IAuthenticationService authenticationService) { _authenticationService = authenticationService; } [HttpPost("register")] public IActionResult Register(RegisterRequest request) { var authResult = _authenticationService.Register( request.FirstName, request.LastName, request.Email, request.Password); var response = new AuthenticationResponse( authResult.Id, authResult.FirstName, authResult.LastName, authResult.Email, authResult.Token); return Ok(response); } [HttpPost("login")] public IActionResult Login(LoginRequest request) { var authResult = _authenticationService.Login( request.Email, request.Password); var response = new AuthenticationResponse( authResult.Id, authResult.FirstName, authResult.LastName, authResult.Email, authResult.Token); return Ok(response); } } 我們已經定義好我們的 service 後,便可以到 presentation 的 Program(或是其它入口點,如 Startup.cs 或 MauiProgram.cs),做 service 的依賴注入。 using BuberDinner.Application.Services.Authentication; var builder = WebApplication.CreateBuilder(args); { builder.Services.AddScoped<IAuthenticationService, AuthenticationService>(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); } var app = builder.Build(); { app.UseSwagger(); app.UseSwaggerUI(); app.UseHttpsRedirection(); app.MapControllers(); app.Run(); } 最後,我們先實作一個暫時的 Service,來確認 Api 是可以作業的。 public class AuthenticationService : IAuthenticationService { public AuthenticationResult Register(string firstName, string lastName, string email, string password) { return new AuthenticationResult( Guid.NewGuid(), firstName, lastName, email, "token" ); } public AuthenticationResult Login(string email, string password) { return new AuthenticationResult( Guid.NewGuid(), "firstName", "lastName", email, "token" ); } } 執行 dotnet run --project .\Mysln.Api\ 在 Swagger 中測試我們實作的 register 與 login API,如果正常工作,會回傳 StatusCode: 200。 Dependency Injection 我們想要每一層都可以自己管理自己的注入,此時我們需要引入 Microsoft.Extensions.DependencyInjection。 接下來實作 Application 的 DependencyInjection。 public static class DependencyInjection { public static IServiceCollection AddApllication(this IServiceCollection services) { services.AddScope<IAuthenticationService, AuthenticationService>(); return services; } } 接下來實作 Infrastructure 的 DependencyInjection。(暫時還沒有注入 repository) public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services) { // 未來要注入 repositories return services; } } 接下來我們可以改寫 Program.cs。 using BuberDinner.Application; using BuberDinner.Infrastructure; var builder = WebApplication.CreateBuilder(args); { builder.Services .AddApplication() .AddInfrastructure(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); } var app = builder.Build(); { app.UseSwagger(); app.UseSwaggerUI(); app.UseHttpsRedirection(); app.MapControllers(); app.Run(); } 實作 JWT Token Generator 首先先在 Application Layer 創建一個 interface 來做依賴反轉 public interface IJwtTokenGenerator { string GenerateToken(Guid userId, string firstName, string lastName); } 接著我們到 Infrastructure Layer 來實作我們的 JwtTokenGenerator。 首先我們需要 System.IdentityModel.Tokens.Jwt 這個 Package。 接著我們實作 JwtTokenGenerator。 public class JwtTokenGenerator : IJwtTokenGenerator { public string GenerateToken(Guid userId, string firstName, string lastName) { var signingCredentials = new SigningCredentials( new SymmetricSecurityKey( Encoding.UTF8.GetBytes("super-secret-key")), SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.GivenName, firstName), new Claim(JwtRegisteredClaimNames.FamilyName, lastName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var securityToken = new JwtSecurityToken( issuer: "Mysln", expires: DateTime.Now.AddDays(1), claims: claims, signingCredentials: signingCredentials); return new JwtSecurityTokenHandler().WriteToken(securityToken); } } 接著我們將之注入到服務中,即大功告成了。 public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services) { services.AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>(); return services; } } 使用 Options Pattern 注入 JWT Settings 接下來我們要使用 Options Pattern 將 JWT Settings 注入到 JwtTokenGenerator 中。 首先我們先到 Mysln.Api 的 appsettings.json 中將 options 設置完成。 { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "JwtSettings": { "Secret": "super-secret-key", "ExpiryMinutes": 60, "Issuer": "Mysln", "Audience:": "Mysln" } } 由於我們要使用 Options Pattern,我們需要改寫我們的 Program.cs,並且將 ConfigurationManager 注入到 Infrastructure 的 DependencyInjection。 為此我們需要引入套件 Microsoft.Extensions.Configuration 與 Microsoft.Extensions.Options.ConfigurationExtensions。 並且我們需要創建一個 Model。 public class JwtSettings { public const string SectionName = "JwtSettings"; public string Secret { get; init; } = null!; public int ExpiryMinutes { get; init; } public string Issuer { get; init; } = null!; public string Audience { get; init; } = null!; } Program.cs 需改寫成: builder.Services .AddApplication() .AddInfrastructure(builder.Configuration); 將 DependencyInjection 改寫成: public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services, ConfigurationManager configuration) { services.Configure<JwtSettings>(configuration.GetSection(JwtSettings.SectionName)); services.AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>(); services.AddSingleton<IDateTimeProvider, DateTimeProvider>(); return services; } } 接下來,我們可以把 JwtTokenGenerator 改寫成: public class JwtTokenGenerator : IJwtTokenGenerator { private readonly JwtSettings _jwtSettings; private readonly IDateTimeProvider _dateTimeProvider; public JwtTokenGenerator(IDateTimeProvider dateTimeProvider, IOptions<JwtSettings> jwtOptions) { _dateTimeProvider = dateTimeProvider; _jwtSettings = jwtOptions.Value; } public string GenerateToken(Guid userId, string firstName, string lastName) { var signingCredentials = new SigningCredentials( new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_jwtSettings.Secret)), SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()), new Claim(JwtRegisteredClaimNames.GivenName, firstName), new Claim(JwtRegisteredClaimNames.FamilyName, lastName), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; var securityToken = new JwtSecurityToken( issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, expires: _dateTimeProvider.UtcNow.AddMinutes(_jwtSettings.ExpiryMinutes), claims: claims, signingCredentials: signingCredentials); return new JwtSecurityTokenHandler().WriteToken(securityToken); } } 以上就大功告成了。 使用 dotnet user-secrets 指令 如果不想要將 Options 中的 secret 儲存在程式(appsettings.json)裡面,可以利用 dotnet user-secrets 將 secret 儲存於環境變數裡面。 透過執行以下的指令來初始化專案的 UserSecretsId dotnet user-secrets init --project Mysln.Api 接著將 UserSecretsId 綁定到我們專案的 JwtSettings:Secret。 dotnet user-secrets set --project Mysln.Api "JwtSettings:Secret" 日後可以經由以下指令查詢。 dotnet user-secrets list --project Mysln.Api Domain Model 先建立一個簡單的 Domain Model(Entity) public class User { public Guid Id { get; set; } = Guid.NewGuid(); public string FirstName { get; set; } = null!; public string LastName { get; set; } = null!; public string Email { get; set; } = null!; public string Password { get; set; } = null!; } Repository Pattern 在 Application Layer 建立 IRepository public interface IUserRepository { User? GetUserByEmail(string email); void Add(User user); } 將 IRepository 注入 Application 的 Service 並用查改存推改寫 Service public class AuthenticationService : IAuthenticationService { private readonly IJwtTokenGenerator _jwtTokenGenerator; private readonly IUserRepository _userRepository; public AuthenticationService(IJwtTokenGenerator jwtTokenGenerator, IUserRepository userRepository) { _jwtTokenGenerator = jwtTokenGenerator; _userRepository = userRepository; } public AuthenticationResult Register(string firstName, string lastName, string email, string password) { // 查 if (_userRepository.GetUserByEmail(email) is not null) { throw new Exception("User with given email already exists."); } // 改 var user = new User { FirstName = firstName, LastName = lastName, Email = email, Password = password }; // 存 _userRepository.Add(user); // 推 var token = _jwtTokenGenerator.GenerateToken(user.Id, firstName, lastName); return new AuthenticationResult(user.Id, firstName, lastName, email, token); } public AuthenticationResult Login(string email, string password) { // 查 if (_userRepository.GetUserByEmail(email) is not User user) { throw new Exception("User with given email does not exist."); } if (user.Password != password) { throw new Exception("Invalid password."); } // 改 var token = _jwtTokenGenerator.GenerateToken(user.Id, user.FirstName, user.LastName); return new AuthenticationResult(user.Id, user.FirstName, user.LastName, email, token); } } 接著我們在 Infrastructure Layer 實作我們的 repository,我們暫時先不接資料庫,所以先做一個 InMemory 版本的 repository 來做測試。 public class UserRepository : IUserRepository { private readonly List<User> _users = new(); public void Add(User user) { _users.Add(user); } public User? GetUserByEmail(string email) { return _users.SingleOrDefault(u => u.Email.Equals(email)); } } 實作完需要透過 DependencyInjection 注入到我們的 Service Container 內。 public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services, ConfigurationManager configuration) { services.Configure<JwtSettings>(configuration.GetSection(JwtSettings.SectionName)); services.AddSingleton<IJwtTokenGenerator, JwtTokenGenerator>(); services.AddSingleton<IDateTimeProvider, DateTimeProvider>(); services.AddSingleton<IUserRepository, UserRepository>(); return services; } } 至此,我們已經完成了一個簡單的身份認證的 API。

<span title='2024-02-26 15:03:15 +0800 +0800'>February 26, 2024</span>&nbsp;·&nbsp;6 min&nbsp;·&nbsp;Rain Hu

[IT] .NET Maui

.NET Maui .NET Maui 是一個跨平台的桌面與手機應用開發框架,它支援 iOS、Android、macOS、Windows。 不同於 Xamarin,.NET Maui 只需要一個專案便可以導向不同的平台。 架構 一個 .NET Maui 專案底下,預設會有幾個資料夾與檔案,其關係如下圖: /Platforms 底下的各個資料夾為不同平台的入口,不同的平台各有一個 Program.cs。 各個 Program.cs 內又會透過注入該 namespace 底下的 AppDelegate ,將入口指向 MauiProgram 的 CreateMauiApp(),就此將不同平台路由到 MauiProgram.cs 這個統一的入口。 public class Program { static void Main(string[] args) { UIApplication.Main(args, null, typeof(AppDelegate)); } } [Register("AppDelegate")] public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); } 關係如下圖: graph TD; iOS-->MauiProgram.cs; Android-->MauiProgram.cs; Windows-->MauiProgram.cs; macOS-->MauiProgram.cs; MauiProgram.cs-->App; App-->AppShell; AppShell-.->Page1; AppShell-.->Page2; AppShell-.->Page3; AppShell-.->Page4; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); return builder.Build(); } } public partial class App : Application { public App() { InitializeComponent(); MainPage = new AppShell(); } } 從上面兩段程式 MauiProgram.cs 與 App.xaml.cs 可以看出,這個統一的進入點會開啟 App,並將 AppShell 作為 MainPage 開放。 Router 在此可以發現 AppShell 作為一個 Controller 的功能,用來引導頁面的路由。 我們可以透過 RegisterRoute 來注冊要顯示的頁面。 public partial class AppShell : Shell { public AppShell() { InitializeComponent(); Routing.RegisterRoute(nameof(Page1), typeof(Page1)); Routing.RegisterRoute(nameof(Page2), typeof(Page2)); Routing.RegisterRoute(nameof(Page3), typeof(Page3)); } } xaml 上述的頁面可以透過新增 xaml 檔來建立: 以下為一個 xaml 檔的 sample x:class="MoneyTrack.AppShell" 表示該檔案的路徑為 MoneyTrack.AppShell mlns 關鍵字很像是 using: xmlns:views="clr-namespace:MoneyTrack.Views": 代表將 MoneyTrack.Views 這個路徑命名成 views。 ShellContent 內代表是首頁要導引至的頁面,如下例會導向 view:MoneyTackPage <?xml version="1.0" encoding="UTF-8" ?> <Shell x:Class="MoneyTrack.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:MoneyTrack" xmlns:views="clr-namespace:MoneyTrack.Views" Shell.FlyoutBehavior="Disabled" Title="MoneyTrack"> <ShellContent Title="Home" ContentTemplate="{DataTemplate views:MoneyTackPage}" Route="MoneyTackPage" /> </Shell> <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MoneyTrack.Views.ContactsPage" Title="Contacts"> <VerticalStackLayout Spacing="5"> <Label Text="Welcome to .NET MAUI!" VerticalOptions="Center" HorizontalOptions="Center" /> <Button x:Name="btn1" Clicked="btn1_Clicked" Text="click1"></Button> <Button x:Name="btn2" Clicked="btn2_Clicked" Text="click2"></Button> </VerticalStackLayout> </ContentPage> Shell 接著我們可以透過 Shell 來控制面版上要顯示的頁面: public void btnPage1_Clicked() { Shell.Current.GoToAsync(nameof(Page1)); } GoToAsync Shell 本身是一個 View,也是一個 Layout GoToAsync 可以用來切換頁面 void btnPage1_Clicked(object sender, EvertArgs e) { Shell.Current.GoToAsync($"{nameof(Page1)}"); // 前往 Page1 } void btnCancel_Clicked(object sender, EvertArgs e) { Shell.Current.GoToAsync($"//{nameof(MainPage)}"); // 回到 MainPage } void btnCancel_Clicked(object sender, EvertArgs e) { Shell.Current.GoToAsync($".."); // 回到上一頁 } QueryProperty 透過 QueryPropertyAttribute 可以達到如 http method 裡的 get 的方法。 以下範例等同於實現 page1?Id=1,在路由到 page1 的同時,將 property: Id 賦值。 [QueryProperty(nameof(PageId), "Id")] public partial class Page1 : ContentPage { public EditContactPage() { InitializeComponent(); } public string ContactId { set { lblName.Text = Id; } } }

<span title='2024-01-28 13:30:34 +0800 +0800'>January 28, 2024</span>&nbsp;·&nbsp;2 min&nbsp;·&nbsp;Rain Hu

[IT] CORS 跨原始來源要求

TL;DR CORS (Cross-Origin Resource Sharing) 是一個支援安全跨源請求和資料傳輸的機制,用於在瀏覽器和伺服器之間進行跨源請求。 這是一個 W3C 標準,可讓伺服器放寬相同原始來源原則。 不是安全性功能,CORS 會放寬安全性。 允許 CORS 並不會增強 API 的安全性。CORS 的運作方式 允許伺服器明確允許某些跨原始來源要求,同時拒絕其他要求。 比舊版技術 (例如:JSONP) 更安全且更有彈性。 何謂相同原始來源 如果兩個 URL 具有相同的配置、主機和連接埠,則其原始來源相同 (RFC 6454)。 這兩個 URL 具有相同的原始來源: https://example.com/foo.html https://example.com/bar.html 這些 URL 的原始來源與前兩個 URL 不同: https://example.net:不同的網域 https://www.example.com/foo.html:不同的子網域 http://example.com/foo.html:不同的配置 https://example.com:9000/foo.html:不同的連接埠 如何啟動 CORS 有三種方式可以啟用 CORS: 在中介軟體中,使用具名原則或預設原則。 使用端點路由。 使用 [EnableCors] 屬性。 搭配具名原則使用 [EnableCors] 屬性能夠以最精細的程度來控制對於支援 CORS 之端點的限制。 ...

<span title='2024-01-19 00:41:51 +0800 +0800'>January 19, 2024</span>&nbsp;·&nbsp;3 min&nbsp;·&nbsp;Rain Hu

[IT] 事件總線 EventBus

EventBus EventBus 用於維護一個事件源與事件處理的映射字典 通過 Singleton,確保 EventBus 的唯一入口 利用反射完成事件源與件事處理的初始化綁定 提供統一的事件注冊(register)、取消注冊(unsubscribe)和觸發(trigger)。 Interfaces IEventData public interface IEventData { DateTime EventTime { get; set; } object EventSource {get; set; } } IEventHandler public interface IEventHandler { } IEventHandler`1 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData { void HandlerEvent(TEventData eventData); } base class EventData public class EventData : IEventData { public DateTime EventTIme { get; set; } object EventSource { get; set; } public EventData() { EventTime = DateTime.Now; } } Domain FishType public enum FishType { None, 鯽魚, 鯉魚, 黑魚, 青魚, 草魚, 鱸魚 } FishingEventData : EventData public class FishingEventData : EventData { public FishType FishType { get; set; } public FishingMan FishingMan { get; set; } } FishingEventHandler : IEventHandler public class FishingEventHandler : IEventHandler<FishingEventData> { public void HandleEvent(FishingEventData eventData) { var type = eventData.FishType; var fishMan = eventData.FishingMan; var Name = fishMan.Name; if (type == FishType.None) { fishMan.Message = string.Format("{0}: 沒有釣到魚, 累計釣了{1}條魚", Name, fishMan.FishCount); } else { fishMan.FishCount++; fishMan.Message = string.Format("{0}: 釣到一條[{2}], 累計釣了{1}條魚", Name, fishMan.FishCount, type); } } } FishingMan public class FishingMan { public string Name { get; set; } public int FishCount { get; set; } public FishingRod FishingRod { get; set; } public string Message { get; set; } public FishingMan(string name) { Name = name; FishCount = 0; } public void Fishing() { FishingRod.ThrowHook(this); } } FishingRod 用反射註冊事件 public class FishingRod { public string Message { get; private set; } public FishingRod() { } public void ThrowHook(FishingMan man) { if (new Random().Next() % 2 == 0) { var type = (FishType)(new Random().Next(0, 5) + 1); Message = ("魚兒上鉤了!"); if (FishingEvent != null) { var eventData = new FishingEventData { FishingMan = man, FishType = type }; EventBus.Default.Trigger<FishingEventData>(eventData); } } else { var type = FishType.None; Message = ("可惜了!"); if (FishingEvent != null) { var eventData = new FishingEventData { FishingMan = man, FishType = type }; EventBus.Default.Trigger<FishingEventData>(eventData); } } } } EventBus 實作 用一個靜態單例來統一管理事件 public class EventBus { public static EventBus Default => new EventBus(); private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping; private EventBus() { _eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>(); MapEventToHandler(); } private void MapEventToHandler() { Assembly assembly = Assembly.GetEntryAssembly(); foreach (var type in assembly.GetTypes()) { if (typeof(IEventHandler).IsAssignableFrom(type)) { Type handlerInterface = type.GetInterface("IEventHandler`1"); if (handlerInterface == null) continue; Type eventDataType = handlerInterface.GetGenericArguments()[0]; if (_eventAndHandlerMapping.ContainsKey(eventDataType)) { List<Type> handlerTypes = _eventAndHandlerMapping[eventDataType]; handlerTypes.Add(type); _eventAndHandlerMapping[eventDataType] = handlerTypes; } else { var handlerTypes = new List<Type> { type }; _eventAndHandlerMapping[eventDataType] = handlerTypes; } } } } public void Register<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler)) { handlerTypes.Add(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } public void Unsubscribe<TEventData>(Type eventHandler) { List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)]; if (!handlerTypes.Contains(eventHandler)) { handlerTypes.Remove(eventHandler); _eventAndHandlerMapping[typeof(TEventData)] = handlerTypes; } } public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData { List<Type> handlers = _eventAndHandlerMapping[eventData.GetType()]; if (handlers != null && handlers.Count > 0) { foreach (var handler in handlers) { MethodInfo methodInfo = handler.GetMethod("HandleEvent"); if (methodInfo != null) { object obj = Activator.CreateInstance(handler); methodInfo.Invoke(obj, new object[] { eventData }); } } } } } demo MacOs Cocoa Project public partial class ViewController : NSViewController { public ViewController (IntPtr handle) : base (handle) { } public override void ViewDidLoad () { base.ViewDidLoad (); jeff = new FishingMan("Jeff"); rod = new FishingRod(); jeff.FishingRod = rod; EventBus eventBus = EventBus.Default; eventBus.Register<FishingEventData>(typeof(FishingEventHandler)); } FishingMan jeff; FishingRod rod; public override NSObject RepresentedObject { get { return base.RepresentedObject; } set { base.RepresentedObject = value; } } partial void Button_Click(NSButton sender) { jeff.Fishing(); TextLabel.StringValue = rod.Message; TextLabel2.StringValue = jeff.Message ?? ""; } }

<span title='2023-09-25 23:50:02 +0800 +0800'>September 25, 2023</span>&nbsp;·&nbsp;3 min&nbsp;·&nbsp;Rain Hu