<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: JasonFu</title>
    <description>The latest articles on DEV Community by JasonFu (@fujisheng).</description>
    <link>https://dev.to/fujisheng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4102456%2F803db54a-a0c5-4a4a-8e86-f03c693feb59.jpg</url>
      <title>DEV Community: JasonFu</title>
      <link>https://dev.to/fujisheng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fujisheng"/>
    <language>en</language>
    <item>
      <title>Generating Binding Code Wasn't Enough: Moving Unity UI Composition to Compile Time</title>
      <dc:creator>JasonFu</dc:creator>
      <pubDate>Tue, 01 Sep 2026 09:47:19 +0000</pubDate>
      <link>https://dev.to/fujisheng/generating-binding-code-wasnt-enough-moving-unity-ui-composition-to-compile-time-4ojd</link>
      <guid>https://dev.to/fujisheng/generating-binding-code-wasnt-enough-moving-unity-ui-composition-to-compile-time-4ojd</guid>
      <description>&lt;p&gt;Source generators are often introduced as a way to remove boilerplate. That is useful, but it was not the main architectural reason FUI moved more of its Unity UI pipeline into Roslyn.&lt;/p&gt;

&lt;p&gt;The harder question begins after binding code has already been generated: does the runtime still need to scan assemblies, inspect attributes, resolve types, and reconstruct the relationship between a View, ViewModel, BindingContext, and Presenter?&lt;/p&gt;

&lt;p&gt;FUI's answer is to move that composition step to compile time. The generator does not stop at property notifications and binding callbacks. It also emits binding factories and strongly typed routes, so the Player runtime executes an already-validated object graph instead of rediscovering it.&lt;/p&gt;

&lt;p&gt;This article explains why that distinction matters, how the design evolved, and what the final architecture gains beyond the vague promise of “less reflection.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The original problem was repetitive protocol code
&lt;/h2&gt;

&lt;p&gt;Consider a settings screen with a title, a volume slider, a vibration toggle, and a close button. The ViewModel is small, but connecting it to the UI requires a surprisingly large protocol:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;propagate property changes to UI elements;&lt;/li&gt;
&lt;li&gt;propagate control changes back to the ViewModel;&lt;/li&gt;
&lt;li&gt;connect UI events to commands;&lt;/li&gt;
&lt;li&gt;perform initial synchronization;&lt;/li&gt;
&lt;li&gt;unsubscribe every handler during unbinding;&lt;/li&gt;
&lt;li&gt;construct the matching BindingContext and Presenter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these steps is individually difficult. The risk comes from repetition. A missing unsubscribe, an incompatible target member, or an incorrect string may remain invisible until that specific screen opens.&lt;/p&gt;

&lt;p&gt;The earliest code-generation experiment preserved in FUI's repository was an external &lt;code&gt;FUICompiler&lt;/code&gt; executable. It targeted .NET 6, was published as a self-contained &lt;code&gt;win-x64&lt;/code&gt; tool, walked Roslyn syntax nodes, extracted binding attributes, and emitted BindingContext source.&lt;/p&gt;

&lt;p&gt;The central idea was already present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;classDeclarations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DescendantNodes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ClassDeclarationSyntax&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;classDeclaration&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;classDeclarations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;Utility&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetClassBindingAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;classDeclaration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Build a binding configuration and emit a BindingContext.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prototype solved the most visible problem: repetitive binding code. But an external compiler also introduced orchestration work. It had to find project files, decide where generated files belonged, coordinate with Unity's import cycle, and keep its own platform and version assumptions in sync with the project.&lt;/p&gt;

&lt;p&gt;Some information was also read as syntax text, such as &lt;code&gt;property.Type.ToString()&lt;/code&gt;, instead of relying fully on the compiler's semantic type system.&lt;/p&gt;

&lt;p&gt;The prototype was removed in the next commit on the same day, so it should not be presented as a long-lived production system. It is better understood as an early design probe: code generation was useful, but the generator belonged inside the compilation pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving generation into Roslyn solved only the first half
&lt;/h2&gt;

&lt;p&gt;FUI later moved the work into C# Source Generators. That immediately improved the build boundary.&lt;/p&gt;

&lt;p&gt;The input was now the active Roslyn &lt;code&gt;Compilation&lt;/code&gt;, not a separate tool's interpretation of a directory. The generator could use &lt;code&gt;SemanticModel&lt;/code&gt; and &lt;code&gt;INamedTypeSymbol&lt;/code&gt; to reason about actual types, inheritance, and attributes. Generated source then participated in the same compilation as user code, allowing ordinary C# errors to expose invalid generated relationships.&lt;/p&gt;

&lt;p&gt;Before the latest architectural refactor, FUI had three generator entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ObservableObjectGenerator&lt;/code&gt; emitted property-change support;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BindingContextGenerator&lt;/code&gt; emitted binding and unbinding logic;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BindingContextInfoGenerator&lt;/code&gt; emitted metadata for Editor tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That system already removed substantial boilerplate. It also grew to support two-way binding, event commands, descriptor metadata, and generic BindingContexts.&lt;/p&gt;

&lt;p&gt;However, generating a BindingContext did not yet make compile time the source of truth for composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The intermediate architecture had two sources of truth
&lt;/h2&gt;

&lt;p&gt;The older runtime still contained a &lt;code&gt;BindingContextTypeResolver&lt;/code&gt;. Its static initialization scanned every loaded assembly and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;assembly&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AppDomain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentDomain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAssemblies&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTypes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;ResolveAllBindingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;ResolveViewModelDefaultPresenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resolver read attributes from generated BindingContext types, discovered Presenter implementations, and built several lookup tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;View name  -&amp;gt; BindingContext type
ViewModel  -&amp;gt; BindingContext type
ViewModel  -&amp;gt; Presenter type
Derived VM -&amp;gt; reusable base BindingContext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a screen was created, the old &lt;code&gt;UIEntity.Create&lt;/code&gt; path queried those tables and then used reflection-based construction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resultViewModelType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bindingContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;contextType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;presenter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;presenterType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reveals a more important problem than the raw cost of reflection: the type relationship was known during generation and then discovered again at runtime.&lt;/p&gt;

&lt;p&gt;The generator decided which BindingContext belonged to a ViewModel. The runtime independently reconstructed that answer through attributes and type scanning. Both rule sets had to remain aligned. If they diverged, the failure appeared when a screen was first instantiated rather than where the relationship was declared.&lt;/p&gt;

&lt;p&gt;Even if the rules never diverged, the runtime still repeated work that compilation had already completed.&lt;/p&gt;

&lt;p&gt;This is a useful distinction for any source-generated framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compile time: generate an implementation.
Runtime: rediscover and compose the generated implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a source generator does not automatically create a compile-time architecture. If runtime metadata interpretation is still required, generation has removed local boilerplate but has not become the system's single source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feature growth turned the resolver into a second compiler
&lt;/h2&gt;

&lt;p&gt;With simple one-way binding, composition answers only a few questions: which View belongs to a ViewModel, and which BindingContext should be constructed?&lt;/p&gt;

&lt;p&gt;As the framework grows, the relationship matrix expands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two-way binding needs a reverse event path and optional &lt;code&gt;ConvertBack&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;commands connect methods, parameters, elements, and events;&lt;/li&gt;
&lt;li&gt;one ViewModel may project to multiple Views;&lt;/li&gt;
&lt;li&gt;Presenter selection may be explicit or inferred;&lt;/li&gt;
&lt;li&gt;routes carry layer, history, cache, transition, and dependency policies;&lt;/li&gt;
&lt;li&gt;list items, prototypes, and dynamic Views need the same binding factories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each feature creates a choice. The runtime resolver can learn another rule, or the generator can emit the final answer.&lt;/p&gt;

&lt;p&gt;Extending the resolver is often the smaller short-term change. Over time, however, it becomes a second compiler: it discovers candidates, applies precedence, resolves ambiguity, handles inheritance, and requires preservation rules for reflection-reachable types in AOT and managed-stripping environments.&lt;/p&gt;

&lt;p&gt;The architectural turning point was therefore not simply replacing &lt;code&gt;ISourceGenerator&lt;/code&gt; with &lt;code&gt;IIncrementalGenerator&lt;/code&gt;. It was changing what the generator produced.&lt;/p&gt;

&lt;p&gt;FUI began generating the complete executable composition relationship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generator now emits the runtime entry points
&lt;/h2&gt;

&lt;p&gt;The current package has one &lt;code&gt;[Generator]&lt;/code&gt; entry point: &lt;code&gt;BindingSourceGenerator&lt;/code&gt;. It uses a syntax provider to identify class declarations, confirms observable types through the semantic model, and produces five categories of source in a single pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ViewModel + attributes
        |
        +-- observable properties: PropertyChanged.g.cs
        +-- binding execution: &amp;lt;VM&amp;gt;.&amp;lt;View&amp;gt;.BindingContext.g
        +-- Editor metadata: &amp;lt;VM&amp;gt;.&amp;lt;View&amp;gt;.BindingInfo.g
        +-- type factories: BindingRegistry.g.cs
        +-- navigation entry points: Routes.g.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first three outputs answer how data moves. The final two answer how objects are constructed and screens are opened. Both sets are derived from the same compilation input.&lt;/p&gt;

&lt;p&gt;The Settings Sample shows the intended developer-facing input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ViewContract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SettingsView"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;RoutePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Layer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Popup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;CoverageMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CoverageMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeepVisible&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SettingsViewModel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ViewModel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ObservableProperty&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Volume"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SliderElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;bindingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BindingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TwoWay&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.5F&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Close"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ButtonElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnClick&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SettingsSampleRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These attributes are not runtime scripts. They form a small declarative language embedded in C#. The generator compiles that language into three layers of executable behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Property changes become ordinary calls
&lt;/h3&gt;

&lt;p&gt;For an &lt;code&gt;[ObservableProperty]&lt;/code&gt; field, FUI emits the property setter, change delegate, and notification path. Updating the value uses compiled field access and delegate calls rather than &lt;code&gt;PropertyInfo.SetValue&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Binding descriptions become symmetric lifecycle code
&lt;/h3&gt;

&lt;p&gt;The generated BindingContext caches target elements and subscribes to ViewModel changes. A two-way binding also subscribes to control changes and writes values back to the ViewModel. &lt;code&gt;OnUnbinding&lt;/code&gt; contains the corresponding unsubscribe path.&lt;/p&gt;

&lt;p&gt;That symmetry is more valuable than saving keystrokes. A common manual error—adding a listener without removing it—becomes a property of one generator template. Once that template is verified, every generated screen follows the same lifecycle protocol.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Type discovery becomes direct construction
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;BindingFactoryGenerator&lt;/code&gt; emits explicit registration code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GeneratedBindingRegistry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SettingsViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettingsViewModel_SettingsView_BindingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettingsPresenter&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime registry still uses &lt;code&gt;Type&lt;/code&gt; as a key because dynamic items may need to resolve a factory from an actual ViewModel type. The value, however, is a compiled &lt;code&gt;Func&lt;/code&gt;. The runtime no longer scans every assembly to discover implementations and no longer asks &lt;code&gt;Activator.CreateInstance&lt;/code&gt; to infer how they should be constructed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RouteGenerator&lt;/code&gt; performs the same transformation for navigation. It emits a &lt;code&gt;Route&amp;lt;TViewModel&amp;gt;&lt;/code&gt; that contains the resource key, ViewModel factory, BindingContext factory, Presenter factory, and route policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SettingsViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SettingsView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;GeneratedRouteFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SettingsViewModel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"SettingsView"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SettingsPresenter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettingsViewModel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettingsViewModel_SettingsView_BindingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SettingsPresenter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="cm"&gt;/* generated route policy */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application receives an executable, strongly typed screen contract rather than a set of strings and types that still need to be interpreted. &lt;code&gt;Routes.Initialize()&lt;/code&gt; also performs idempotent binding-registry initialization, so navigation and binding no longer maintain separate startup protocols.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the final architecture actually gains
&lt;/h2&gt;

&lt;p&gt;“Less reflection means faster” is too narrow and would require benchmarks to quantify. FUI's repository does not publish a benchmark that supports a percentage claim.&lt;/p&gt;

&lt;p&gt;What the code does establish is that several categories of Player work have been removed from the main binding and navigation path: whole-assembly &lt;code&gt;GetTypes()&lt;/code&gt;, runtime attribute discovery, and reflection-based BindingContext/Presenter construction.&lt;/p&gt;

&lt;p&gt;The broader benefit appears at four stages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Older responsibility&lt;/th&gt;
&lt;th&gt;Generated architecture&lt;/th&gt;
&lt;th&gt;Practical effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development&lt;/td&gt;
&lt;td&gt;Maintain scattered composition code&lt;/td&gt;
&lt;td&gt;Express intent with attributes and symbol-backed names&lt;/td&gt;
&lt;td&gt;Renames and reviews can follow more real references&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compilation&lt;/td&gt;
&lt;td&gt;Generate local binding implementation&lt;/td&gt;
&lt;td&gt;Generate properties, bindings, factories, and routes together&lt;/td&gt;
&lt;td&gt;One input produces the complete relationship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen creation&lt;/td&gt;
&lt;td&gt;Scan types, inspect attributes, invoke reflection constructors&lt;/td&gt;
&lt;td&gt;Look up and call compiled delegates&lt;/td&gt;
&lt;td&gt;The execution path is shorter and deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework evolution&lt;/td&gt;
&lt;td&gt;Teach the runtime resolver every new dimension&lt;/td&gt;
&lt;td&gt;Extend the compile-time model and generated output&lt;/td&gt;
&lt;td&gt;Complexity remains inspectable and testable before Player execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Failures move earlier
&lt;/h3&gt;

&lt;p&gt;Reflection can preserve an incomplete relationship until the screen opens. Generated code must compile. Invalid types, missing constructors, and unresolved route dependencies can surface during the build instead.&lt;/p&gt;

&lt;p&gt;The current generator also reports some architecture-level diagnostics, including missing route dependencies, static dependency cycles, and auto-properties that are incompatible with Pure mode.&lt;/p&gt;

&lt;p&gt;This is not complete compile-time validation. Several syntax actions in the current &lt;code&gt;AttributeBindingAnalyzer&lt;/code&gt; remain disabled, so it would be inaccurate to claim that every binding error is detected by the compiler. The defensible claim is that the failure boundary has moved substantially earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  AOT and stripping tools can see more of the path
&lt;/h3&gt;

&lt;p&gt;Unity Player builds commonly involve IL2CPP and managed-code stripping. Reflection is possible in that environment, but types reached only through runtime scanning often require additional preservation metadata.&lt;/p&gt;

&lt;p&gt;Generated generic references, &lt;code&gt;new&lt;/code&gt; expressions, and direct delegates make more of the real call graph visible to the toolchain. This reduces uncertainty; it does not guarantee that all AOT or stripping concerns disappear.&lt;/p&gt;

&lt;p&gt;FUI also does not eliminate reflection everywhere. Editor-side catalogs, validators, and IL post-processing coordination still inspect loaded assemblies. Those tools serve authoring and build-time workflows, not the Player's primary binding/navigation path. That boundary is more useful than a marketing claim of “zero reflection.”&lt;/p&gt;

&lt;h3&gt;
  
  
  Strongly typed routes turn page identity into a contract
&lt;/h3&gt;

&lt;p&gt;In the older design, View names, ViewModel types, Presenter types, and screen policies lived in different places and were assembled at runtime. A generated &lt;code&gt;Route&amp;lt;TViewModel&amp;gt;&lt;/code&gt; brings them into one object.&lt;/p&gt;

&lt;p&gt;This is not only an autocomplete improvement. Dependencies, synchronous-open support, caching, history, and transition policy can all be decided while generating the route. Navigation receives an executable contract rather than a bag of parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generated code is inspectable architecture output
&lt;/h3&gt;

&lt;p&gt;A reflection resolver's result lives in runtime dictionaries and is usually visible only after the application starts. A generator's result is ordinary C#.&lt;/p&gt;

&lt;p&gt;When behavior is unexpected, the investigation path becomes concrete: inspect the attribute input, inspect the generated source, then inspect runtime execution. There is less need to guess which types an assembly scan found or which precedence rule ran first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FUI has Pure and Mixed property modes
&lt;/h2&gt;

&lt;p&gt;A source generator can add source to a &lt;code&gt;Compilation&lt;/code&gt;; it cannot rewrite an existing method body.&lt;/p&gt;

&lt;p&gt;If the developer declares a field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ObservableProperty&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the generator can add a property in another part of the same &lt;code&gt;partial&lt;/code&gt; type. That is Pure mode.&lt;/p&gt;

&lt;p&gt;If the developer has already written an auto-property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ObservableProperty&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;Volume&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the generator cannot redeclare that property or inject notification logic into its setter. Mixed mode therefore uses optional IL Post Processing with Mono.Cecil to rewrite the setter after compilation, while the source generator still emits the notification entry point and binding code.&lt;/p&gt;

&lt;p&gt;This is not a cosmetic style choice. It follows directly from the additive nature of Source Generators. Pure mode is easier to inspect for new projects; Mixed mode reduces migration cost for codebases that already rely heavily on auto-properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incremental pipeline still has room to improve
&lt;/h2&gt;

&lt;p&gt;The current entry point uses the Incremental Generator API, but it collects all candidate types and combines them with the complete &lt;code&gt;Compilation&lt;/code&gt; before producing assembly-level output.&lt;/p&gt;

&lt;p&gt;That is a legitimate incremental architecture, but not the most granular possible cache graph. Some changes can still cause the aggregated stage to regenerate an entire assembly's outputs.&lt;/p&gt;

&lt;p&gt;This is an implementation detail worth improving, but it does not change the main architectural result: the runtime no longer maintains a second type-discovery system.&lt;/p&gt;

&lt;p&gt;Source generation is also not automatically the best choice for every UI project. A small application with a handful of screens may be clearer with direct handwritten wiring. A system whose relationships truly exist only at runtime may still need reflection or data-driven registration.&lt;/p&gt;

&lt;p&gt;Compile-time generation becomes compelling when the rules are stable, the relationships are repetitive, and the project benefits from validating them before a screen opens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real choice is when uncertainty should be resolved
&lt;/h2&gt;

&lt;p&gt;FUI's evolution can be summarized in three stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;External compiler: remove repetitive binding code.
Early Source Generators: generate inside compilation, but rediscover composition at runtime.
Compile-time composition: emit properties, bindings, factories, and routes; execute them directly at runtime.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason to use Source Generators is therefore not that reflection is inherently bad, nor simply that attributes are shorter to write.&lt;/p&gt;

&lt;p&gt;Most relationships between a View, ViewModel, BindingContext, Presenter, and Route are already known when the code is written. Once those relationships are stable, there is little value in waiting until a screen opens to infer them again.&lt;/p&gt;

&lt;p&gt;Moving that work into compilation produces a shorter runtime path, a clearer type contract, and an earlier failure boundary. Reduced boilerplate is the visible result. Making generated code the single composition truth is the deeper architectural change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/440ada52cbfc7e7a68fbe879b253ee7e317e4910/FUICompiler/FUICompiler.csproj" rel="noopener noreferrer"&gt;Early external FUICompiler project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/440ada52cbfc7e7a68fbe879b253ee7e317e4910/FUICompiler/AttributeBindingContextGenerator.cs" rel="noopener noreferrer"&gt;Early attribute-based BindingContext generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/efbb1f6899e5fe08ca4048ae1e53f6f82fae3eae/Runtime/Core/BindingContext/BindingContextTypeResolver.cs" rel="noopener noreferrer"&gt;Pre-refactor BindingContextTypeResolver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/efbb1f6899e5fe08ca4048ae1e53f6f82fae3eae/Runtime/Core/UIEntity/UIEntity.Creator.cs" rel="noopener noreferrer"&gt;Pre-refactor reflection construction path&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/66f5a92007ecb5eebe57f36b7b03a759eb16146b/Packages/com.fujisheng.fui/FUI.Generators~/Generators/BindingSourceGenerator.cs" rel="noopener noreferrer"&gt;Current incremental generator entry point&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/66f5a92007ecb5eebe57f36b7b03a759eb16146b/Packages/com.fujisheng.fui/FUI.Generators~/Generators/BindingFactoryGenerator.cs" rel="noopener noreferrer"&gt;Current binding factory generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/66f5a92007ecb5eebe57f36b7b03a759eb16146b/Packages/com.fujisheng.fui/FUI.Generators~/Generators/RouteGenerator.cs" rel="noopener noreferrer"&gt;Current strongly typed route generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fujisheng/FUI/blob/66f5a92007ecb5eebe57f36b7b03a759eb16146b/Packages/com.fujisheng.fui/Samples~/Settings%20Sample/Scripts/SettingsViewModel.cs" rel="noopener noreferrer"&gt;Settings Sample input&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/fujisheng/FUI" rel="noopener noreferrer"&gt;fujisheng/FUI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>unity3d</category>
      <category>csharp</category>
      <category>gamedev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Source generation is only half of a Unity UI binding solution</title>
      <dc:creator>JasonFu</dc:creator>
      <pubDate>Mon, 31 Aug 2026 11:16:16 +0000</pubDate>
      <link>https://dev.to/fujisheng/source-generation-is-only-half-of-a-unity-ui-binding-solution-2dep</link>
      <guid>https://dev.to/fujisheng/source-generation-is-only-half-of-a-unity-ui-binding-solution-2dep</guid>
      <description>&lt;p&gt;Source generators are often introduced as a way to remove boilerplate. In a Unity UI project, I think their more useful role is to make a binding contract visible before Play Mode.&lt;/p&gt;

&lt;p&gt;A ViewModel can state its intent directly:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ObservableProperty]
[Bind("Title", nameof(TextElement.Content))]
string title = "Settings";

[Command("Close", nameof(ButtonElement.OnClick))]
public void Close() { }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;From that metadata, a generator can create binding and command code, and an analyzer can report invalid Element names, component types, members, duplicated contracts, or incomplete declarations. A field rename is no longer just a change to one C# identifier; there is enough information for tooling to tell that the UI contract has changed too.&lt;/p&gt;

&lt;p&gt;But C# compilation cannot inspect a prefab. That is the half that gets missed when people talk about compile-time binding. The asset might no longer contain Title, the object might have the wrong component, or a duplicate name might have appeared in the hierarchy.&lt;/p&gt;

&lt;p&gt;My approach in FUI is to pair source generation with prefab-side validation. The editor validates Elements, types, names, nesting boundaries, and page structure when a prefab is saved. The same checks run before a build. The generator and the editor are checking different sides of the same contract.&lt;/p&gt;

&lt;p&gt;The generator also creates typed Route entries. Each route keeps the asset key, ViewModel, Presenter, binding factory, and page policy together, so a project does not need a separate string registry to keep those parts aligned.&lt;/p&gt;

&lt;p&gt;The goal is not to claim that every UI error can be caught statically. It is to move the errors that can be described into a place where a refactor can expose them early.&lt;/p&gt;

&lt;p&gt;I put the implementation and a small Settings Sample here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fujisheng/FUI" rel="noopener noreferrer"&gt;https://github.com/fujisheng/FUI&lt;/a&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
    </item>
  </channel>
</rss>
