EclipseUI 的类 Razor 声明式 UI 语法
EUI (Eclipse UI) 是一种类似 Razor 的声明式 UI 语法,用于构建 EclipseUI 应用界面。它结合了 XML 标记和 C# 代码,通过 Source Generator 实现零反射、强类型的编译时代码生成。
<!-- HomePage.eui -->
@using Eclipse.Controls
<StackLayout Spacing="16" Padding="20">
<Label Text="你好 EclipseUI!" FontSize="32" />
<Button Text="点击我" OnClick="@OnButtonClick" />
</StackLayout>
@code {
private int _count = 0;
private void OnButtonClick(object? sender, EventArgs e)
{
_count++;
StateHasChanged();
}
}引入命名空间,与 C# 的 using 语句相同。Source Generator 会根据 @using 指令解析控件类型。
@using Eclipse.Controls
@using Eclipse.Input
@using MyApp.CustomControls重要:所有使用的控件类型都必须通过 @using 引入其命名空间,否则会生成警告(ECGEN003)。
指定生成的组件命名空间。
@namespace MyApp.Pages
<StackLayout>
<!-- ... -->
</StackLayout>命名空间推断规则(按优先级):
@namespace指令指定的值- MSBuild
RootNamespace+ 相对于项目目录的路径 - 默认值
Eclipse.Generated
示例:
项目: MyApp.csproj (<RootNamespace>MyApp</RootNamespace>)
文件: Pages/Admin/Users.eui
结果命名空间: MyApp.Pages.Admin
定义 C# 代码块,包含字段、属性、方法等。
@code {
private string _message = "Hello";
public string Title { get; set; } = "首页";
private void HandleClick(object? sender, EventArgs e)
{
// 处理逻辑
}
}指定组件的基类。
@inherits LayoutComponentBase
<StackLayout>
@ChildContent
</StackLayout>默认基类为 ComponentBase。
注入依赖服务。
@inject IUserService UserService
@inject ILoggingService Logger
<Label Text="@UserService.CurrentUser.Name" />生成的代码:
[Inject]
public IUserService UserService { get; set; } = null!;
[Inject]
public ILoggingService Logger { get; set; } = null!;为生成的类添加特性。
@attribute [Obsolete("使用 NewPage 替代")]| 控件 | 说明 | 常用属性 |
|---|---|---|
StackLayout |
堆叠布局(垂直/水平) | Orientation, Spacing, Padding, BackgroundColor |
HStack |
水平堆叠布局(StackLayout 的简化版) | Spacing, Padding, BackgroundColor |
Grid |
网格布局 | RowCount, ColumnCount, RowSpacing, ColumnSpacing |
ScrollView |
滚动视图 | ScrollX, ScrollY, VerticalScrollBarVisible, HorizontalScrollBarVisible |
Container |
容器控件 | BackgroundColor, Padding, CornerRadius |
| 控件 | 说明 | 常用属性 |
|---|---|---|
Label |
文本显示 | Text, FontSize, Color, FontWeight, TextAlignment |
Button |
按钮 | Text, BackgroundColor, TextColor, CornerRadius, OnClick |
TextInput |
文本输入 | Text, Placeholder, IsPassword, FontSize, Padding |
CheckBox |
复选框 | IsChecked, Label, CheckedColor, Size |
Image |
图片显示 | Source, Width, Height, Stretch |
所有控件属性都是强类型的,Source Generator 会根据属性类型自动转换字面量值。
数值属性使用双引号包裹,Generator 自动转换为正确类型:
<Label FontSize="24" />
<StackLayout Spacing="16" Padding="8" />
<Button CornerRadius="8" />
<TextInput Padding="8" FontSize="14" />支持的数值类型:int, double, float, long, byte, etc.
<CheckBox IsChecked="true" />
<TextInput IsPassword="false" />
<Button IsEnabled="true" />
<ScrollView VerticalScrollBarVisible="true" />枚举值直接写名称,Generator 自动添加类型前缀:
<StackLayout Orientation="Vertical" />
<HStack Orientation="Horizontal" />
<Label TextAlignment="Center" />
<Image Stretch="Uniform" />生成的代码:
stackLayout.Orientation = Orientation.Vertical;
label.TextAlignment = TextAlignment.Center;
image.Stretch = Stretch.Uniform;支持多种颜色格式:
<!-- 十六进制 -->
<Label Color="#FF0000" />
<Label Color="#80FF0000" /> <!-- 带 Alpha -->
<!-- 颜色名称 -->
<Label Color="Red" />
<Label Color="Blue" />
<!-- RGB 格式 -->
<Label Color="rgb(255,0,0)" />
<Label Color="rgba(255,0,0,0.5)" />生成的代码:
label.Color = Color.FromHex("#FF0000");
label.Color = Colors.Red;
label.Color = Color.Parse("rgb(255,0,0)");支持多种格式:
<!-- 统一边距 -->
<StackLayout Padding="16" />
<!-- 生成: new Thickness(16) -->
<!-- 水平、垂直 -->
<StackLayout Padding="16,8" />
<!-- 生成: new Thickness(16, 8) -->
<!-- 左、上、右、下 -->
<Button Margin="10,20,10,20" />
<!-- 生成: new Thickness(10, 20, 10, 20) --><Point Position="100,50" /> <!-- new Point(100, 50) -->
<Size Size="200,100" /> <!-- new Size(200, 100) -->
<Vector Offset="10,20" /> <!-- new Vector(10, 20) --><Rect Bounds="0,0,100,50" />
<!-- 生成: new Rect(0, 0, 100, 50) --><!-- TimeSpan -->
<Duration Value="00:01:30" />
<!-- 生成: TimeSpan.Parse("00:01:30") -->
<!-- DateTime -->
<Date Value="2024-01-15" />
<!-- 生成: DateTime.Parse("2024-01-15") -->
<!-- Guid -->
<Id Value="550e8400-e29b-41d4-a716-446655440000" />
<!-- 生成: Guid.Parse("...") -->
<!-- Uri -->
<Link Url="https://example.com" />
<!-- 生成: new Uri("https://example.com") -->使用 @ 前缀绑定 C# 表达式或变量:
<!-- 变量绑定 -->
<Label Text="@_message" FontSize="@_fontSize" />
<!-- 表达式绑定 -->
<Label Text="@($"计数: {_count}")" />
<Button IsEnabled="@_count > 0" />
<!-- 属性绑定 -->
<Label Text="@CurrentUser.Name" />@if (_isLoggedIn)
{
<Label Text="欢迎回来!" Color="Green" />
}
else
{
<Label Text="请登录" Color="Red" />
}<StackLayout Spacing="8">
@foreach (var item in _items)
{
<Label Text="@item.Name" FontSize="14" />
}
</StackLayout><Button Text="保存" OnClick="@OnSaveClick" />@code {
private void OnSaveClick(object? sender, EventArgs e)
{
SaveData();
StateHasChanged();
}
}<Button Text="增加" OnClick="@(s => _count++)" /><TextInput
Text="@_inputText"
OnKeyDown="@OnKeyDown"
OnTextInput="@OnTextInput" />@code {
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
SubmitText();
}
}
}<!-- 垂直堆叠 -->
<StackLayout Orientation="Vertical" Spacing="10" Padding="16">
<Label Text="标题" FontSize="24" />
<Label Text="副标题" FontSize="16" />
<Button Text="按钮" />
</StackLayout>
<!-- 水平堆叠 -->
<HStack Spacing="8">
<Button Text="取消" BackgroundColor="#999" />
<Button Text="确定" />
</HStack>属性说明:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Orientation |
Orientation | Vertical | 堆叠方向(Vertical / Horizontal) |
Spacing |
double | 0 | 子元素间距 |
Padding |
double | 0 | 内边距 |
BackgroundColor |
string | null | 背景颜色 |
<Grid RowCount="3" ColumnCount="2" RowSpacing="10" ColumnSpacing="10">
<Label Grid.Row="0" Grid.Column="0" Text="标题" />
<Label Grid.Row="0" Grid.Column="1" Text="副标题" />
<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Text="底部按钮" />
</Grid><ScrollView VerticalScrollBarVisible="true">
<StackLayout Spacing="16">
@foreach (var item in _longList)
{
<Label Text="@item" />
}
</StackLayout>
</ScrollView><Label
Text="Hello EclipseUI"
FontSize="24"
Color="#333333"
FontWeight="Bold"
TextAlignment="Center" />| 属性 | 类型 | 说明 |
|---|---|---|
Text |
string | 显示文本 |
FontSize |
double | 字体大小(默认 14) |
Color |
string | 文本颜色 |
FontWeight |
string | 字体粗细(Normal, Bold) |
TextAlignment |
TextAlignment | 对齐方式(Left, Center, Right) |
<Button
Text="提交"
BackgroundColor="#007AFF"
TextColor="White"
FontSize="16"
CornerRadius="8"
OnClick="@OnSubmit" />| 属性 | 类型 | 说明 |
|---|---|---|
Text |
string | 按钮文本 |
BackgroundColor |
string | 背景颜色(默认 #007AFF) |
TextColor |
string | 文本颜色(默认 White) |
FontSize |
double | 字体大小(默认 14) |
CornerRadius |
double | 圆角半径(默认 4) |
IsEnabled |
bool | 是否启用 |
OnClick |
EventHandler | 点击事件 |
<TextInput
Text="@_username"
Placeholder="请输入用户名"
FontSize="14"
CornerRadius="4"
Padding="8" />| 属性 | 类型 | 说明 |
|---|---|---|
Text |
string | 输入文本 |
Placeholder |
string | 占位提示 |
IsPassword |
bool | 是否为密码输入 |
FontSize |
double | 字体大小(默认 14) |
CornerRadius |
double | 圆角半径(默认 4) |
Padding |
double | 内边距(默认 8) |
支持键盘操作:
Back/Delete- 删除字符Left/Right- 移动光标Home/End- 光标到开头/末尾
| 策略 | 说明 |
|---|---|
Direct |
直接事件,仅在源元素触发 |
Bubble |
冒泡事件,从源元素向上传播到根 |
Tunnel |
隧道事件,从根向下传播到源元素 |
PointerPressed // 指针按下(Bubble)
PreviewPointerPressed // 指针按下(Tunnel)
PointerMoved // 指针移动
PointerReleased // 指针释放
PointerEntered // 指针进入(Direct)
PointerExited // 指针离开(Direct)
PointerWheelChanged // 滚轮滚动
Tapped // 点击KeyDown // 按键按下(Bubble)
PreviewKeyDown // 按键按下(Tunnel)
KeyUp // 按键释放
TextInput // 文本输入调用 StateHasChanged() 触发重新渲染:
@code {
private int _count = 0;
private void Increment()
{
_count++;
StateHasChanged(); // 触发 UI 更新
}
}注意:组件使用脏标记机制,只有调用 StateHasChanged() 后才会重新构建。
Source Generator 会报告以下诊断信息:
| 诊断码 | 严重性 | 说明 |
|---|---|---|
| ECGEN001 | Error | 组件生成失败 |
| ECGEN002 | Error | EUI markup 解析错误 |
| ECGEN003 | Warning | 控件类型未找到 |
示例:
拼写错误的控件名:
<Lable Text="Hello" /> <!-- 正确应为 Label -->警告信息:
warning ECGEN003: Control type 'Lable' not found in 'HomePage.eui'.
Make sure the type exists and the namespace is imported via @using.
将重复使用的 UI 封装为独立组件:
<!-- Card.eui -->
@using Eclipse.Controls
<Container BackgroundColor="#FFFFFF" Padding="16" CornerRadius="8">
<StackLayout Spacing="8">
<Label Text="@Title" FontSize="18" FontWeight="Bold" />
<Label Text="@Description" FontSize="14" Color="#666" />
</StackLayout>
</Container>
@code {
public string Title { get; set; } = "";
public string Description { get; set; } = "";
}属性是强类型的,使用字面量时 Generator 会自动转换:
<!-- ✅ 正确 - 字面量会自动转换 -->
<Label FontSize="24" />
<StackLayout Spacing="16" />
<!-- ✅ 正确 - 表达式绑定 -->
<Label FontSize="@fontSize" />
<!-- ❌ 错误 - 类型不匹配 -->
<Label FontSize="abc" /> <!-- 无法转换为 double -->使用 e.Handled = true 停止事件传播:
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
e.Handled = true; // 阻止后续处理
SubmitForm();
}
}