Skip to content

DeclarativeSurface pt BR

Edgar Mesquita edited this page Aug 14, 2026 · 4 revisions

A superfície declarativa (autoria sem new)

🌐 Esta página em: English · Português

Uma tela é C# puro em expressões: sem linguagem de marcação, sem cerimônia de builder, e sem new:

public override VisualNode Build(ComponentContext context) =>
    Column(gap: Space.S4, children: [
        Text($"Contagem: {_count}", TypeRole.Display, context.Theme.TextPrimary),
        Row(gap: Space.S3, children: [
            Button("Somar", onPressed: () => SetState(() => _count++)),
            Button("Zerar", Variant.Outline, onPressed: () => SetState(() => _count = 0)),
        ]),
    ]);

Todo nome ali é um método fábrica chamado exatamente como o tipo que ele retorna. Não há import para escrever: o SDK coloca a superfície do framework em escopo em cada arquivo do seu projeto, e os seus próprios componentes entram nela automaticamente (veja abaixo).

Como os estilos são valores tipados em vez de strings CSS, o compilador confere a interface inteira, layout e estilo incluídos, e a mesma classe renderiza no web e no nativo pelo Photon.

O contrato

  • Uma fábrica é chamada exatamente como o tipo dela. Column, Text, Button.
  • Ela espelha um construtor parâmetro por parâmetro (mesmos nomes, mesma ordem, mesmos padrões), então argumentos nomeados atravessam de new X(…) para X(…) sem mudança.
  • Nós contêineres recebem um parâmetro children ao final, escrito como expressão de coleção.
  • Não há sobrecargas. A superfície transpila para um gêmeo JavaScript, e métodos JS não podem sobrecarregar, então cada tipo tem UMA fábrica canônica.

Layout é parâmetro, não inicializador

Desde 0.2.0-preview.13

Row(gap: Space.S3, main: MainAlign.SpaceBetween, cross: CrossAlign.Start, children: [])
Column(gap: Space.S2, wrap: true, runGap: Space.S4, children: [])
Text("42", TypeRole.Display, align: TextAlignment.Center, tabular: true)

Row e Column recebem main, cross, wrap, runGap e padding; Text recebe align, mono, tabular e styleOverride. Essas eram propriedades init-only, então definir uma exigia um inicializador de objeto, que exige new, e new é exatamente o que essa superfície remove. Uma linha que precisava centralizar o conteúdo caía fora da superfície inteira e tinha que ser escrita do jeito antigo.

Largura, altura, fundo e raio de canto deliberadamente não são parâmetros de um flex: um flex carregando isso é um Box embrulhando um flex, e as propriedades já dizem isso.

O styleOverride do Text é a saída de uma escala de tipo fechada. Os degraus são o padrão certo, e um design que passa por cima deles em todo lugar deixou de ter escala, mas uma escala fechada sem saída acaba contornada aninhando um HtmlElement cru, o que é pior, porque só funciona num alvo.

Quebra na 0.2.0-preview.13. children vem ao final (o contrato de contêiner), então os botões ficam entre ele e o gap: Column(Space.S3, [ … ]) vira Column(Space.S3, children: [ … ]). Uma palavra, e já é a forma normal da superfície.

Propriedades init mais raras mantêm a forma construtor + inicializador; as fábricas são açúcar sobre os mesmos tipos, nunca uma segunda API:

Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
    Text("Continua o mesmo Box", TypeRole.BodyM))

Records de valor (GridTrack, DialogAction, NavItem) deliberadamente não têm fábricas: eles são dado, e o new(…) com tipo alvo já lê bem.

Os seus componentes entram nela

Defina um componente; o build gera a fábrica dele. Nada para registrar, nada para importar:

// Components/StatTile.cs
public sealed class StatTile : StatelessComponent
{
    public StatTile(string label, string value) { Label = label; Value = value; }
    public string Label { get; init; }
    public string Value { get; init; }
    public override VisualNode Build(ComponentContext context) => /* … */;
}
// Pages/HomePage.cs, sem using, sem new
Row(gap: Space.S3, children: [
    StatTile("Contagem", $"{_count}"),
    StatTile("Dobro", $"{_count * 2}"),
])

Um gerador de código lê a compilação, encontra os componentes, e escreve uma classe estática AppUI mais o global using static que a põe em escopo. Ele fica em passo com os seus componentes porque é gerado a partir deles.

Páginas não ganham fábrica. Uma [Page] é alcançada pela rota dela, nunca composta na mão.

Qual construtor é espelhado

O mais largo, a mesma regra que o transpilador aplica quando colapsa sobrecargas de construtor, para que a fábrica e o construtor emitido nunca discordem. Quando o mais largo não é o que você quer oferecer, eleja outro:

public sealed class Badge : StatelessComponent
{
    [UiFactory]
    public Badge(string label) {}              // ← este ganha a fábrica
    public Badge(string label, int count, bool dot) {}
}

Diagnósticos

Código Severidade Significado
EQ3101 Erro Dois construtores de um componente estão marcados com [UiFactory]. Uma eleição precisa de um único vencedor.
EQ3102 Aviso Dois componentes compartilham um nome, então só um pode ser dono daquela fábrica. Renomeie um, ou construa o outro com new.

Desligando

<PropertyGroup>
  <EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
</PropertyGroup>

Os seus componentes passam a compor com new, exatamente como antes.

A única aresta afiada: uma fábrica sombreia o tipo dela

Um método chamado como um tipo sombreia aquele tipo em qualquer arquivo onde a superfície esteja em escopo. Isso só morde tipos que chegam por um using, ou seja, os do framework, não os seus:

Spacer.Fixed(34)   // ✗ CS0119: `Spacer` liga ao método fábrica
Panel.Empty("x")   // ✓ seu próprio tipo, declarado no seu namespace, vence

O C# resolve nomes declarados no namespace atual antes dos imports using static, então os seus componentes nunca são sombreados pelas próprias fábricas, e você nunca precisa do contorno abaixo.

Os statics do próprio framework que ficam atrás de um nome de fábrica ganham uma fábrica com nome próprio:

Em vez de Escreva
Spacer.Fixed(34) Gap(34)
Badge.AsDot(variant) DotBadge(variant)
new Icon(packGlyph) Glyph(packGlyph)

Desde 0.2.0-preview.7 (a linha do Glyph; as outras duas desde 0.2.0-preview.2)

Um teste de conformidade percorre cada fábrica, procura statics no tipo que ela sombreia, e falha nomeando-os até que cada um tenha uma fábrica com nome próprio, para que uma quarta não possa aparecer despercebida.

O Glyph está lá por uma razão um pouco diferente das outras duas, e vale saber qual: o Icon tem dois construtores, um recebendo o enum Icons curado do framework, outro recebendo o IconGlyph que um pacote de ícones entrega. O Icon(...) espelhado só pode ser um deles, e não há sobrecargas aqui, então um glifo de pacote não tinha caminho nenhum para dentro de um arquivo que importa essa superfície: só podia ser desenhado com new, no único lugar onde o framework promete que você nunca precisa dele.

using eQuantic.UI.MaterialSymbols;

Glyph(MaterialSymbolsIcons.PlayArrowRounded)      // o catálogo de qualquer pacote de ícones
Icon(Icons.Check)                                  // o conjunto curado

Como isso chega ao browser

A superfície gerada é escrita em disco (EmitCompilerGeneratedFiles) porque o eqc lê arquivos, não a compilação C#: uma fábrica que a sua página chama tem que fazer parte do que o transpilador enxerga, ou a chamada resolve para nada e o JavaScript emitido degrada em silêncio. Depois ela é transpilada num módulo como qualquer outra classe, então AppUI.statTile(…) existe no bundle ao lado de UI.column(…).

Se você renomear ou remover um gerador, rode dotnet clean. Arquivos gerados pelo compilador não são removidos quando o gerador que os escreveu some, e o eqc continuaria lendo o resto: um tipo fantasma, ou uma segunda cópia de um vivo.

Veja também

Clone this wiki locally