Aspnetcore: Vorlagenbasierte Komponenten

Erstellt am 27. März 2018  ·  60Kommentare  ·  Quelle: dotnet/aspnetcore

Sie sollten beispielsweise in der Lage sein, eine Grid- oder ListView-Komponente zu erstellen, die mithilfe einer Define-Vorlage an jedes Element in einer Auflistung bindet.

area-blazor

Hilfreichster Kommentar

Meine persönliche Meinung ist, dass Razor so nah wie möglich an HTML-Idiomen bleiben sollte, anstatt zu den Idiomen abzudriften, die wir in XAML sehen. Das würde bedeuten, dass Sie eher wie die Hierarchie der Elemente aussehen, die Sie in <table> und nicht wie typische XAML-Dinge. Wir könnten generisch genug sein, um beides zu unterstützen, aber ich bin im Moment immer noch sehr fest im Lager von "Razor ist HTML".

Alle 60 Kommentare

Dies bedeutet insbesondere, dass wir eine Möglichkeit benötigen, um zu signalisieren, dass ein bestimmter Teil des untergeordneten Inhalts innerhalb einer Komponente als Action<RenderTreeBuilder, T> kompiliert werden soll, für einen Typ T, den wir dynamisch aus der Verwendung beim Entwurf auflösen müssen Zeit. Dann ist unser Design-Time-Codegen in der Lage, den richtigen Intellisense-Kontext zu erzeugen, wenn Razor-Ausdrücke in dieses untergeordnete Element geschrieben werden.

Beispiel:

<MyListWithHeader RowItems=@People>
    <Title>
        Here I'm still in the outer context so can reference properties on the component itself 
        such as <strong i="8">@TitleText</strong>
    </Title>
    <RowTemplate> <!-- Somehow need intellisense to know that RowTemplate is meaningful here, and that its "item" value is taken from the generic type arg on the RowItems property -->
        Here I'm in the context of an Action<RenderTreeBuilder, Person> because we supplied
        an IEnumerable<Person> as the value for RowItems, so I can write @item.FirstName
    </RowTemplate>
</MyListWithHeader>

<strong i="9">@functions</strong>
{
    string TitleText { get; set; }
    IEnumerable<Person> People { get; set; }

    class Person
    {
        public string FirstName { get; set; }
    }
}

Ich bin mir nicht sicher, wie allgemein wir diese Funktion machen wollen. Möglicherweise weniger allgemein als in der obigen Skizze angegeben. @rynowak wäre der Experte dafür, was hier sinnvoll umzusetzen ist. Die ganze Komplexität liegt zur Zeit des Entwurfs/der Kompilierung. Es erfordert keine Laufzeitunterstützung, die ich mir vorstellen kann.

Hallo, habe versucht, dies zu erreichen, indem ich selbst ein IEnumerable übergeben habeAls Parameter konnte ich ein Raster erzeugen, aber ich konnte es nicht aktualisieren, wenn sich der Parameter änderte. Dies ist der Code:

<table class="@ClassName">
  <tr>
    <strong i="7">@foreach</strong> (var col in Columns)
    {
      <th>@col</th>
    }
  </tr>

  <strong i="8">@foreach</strong> (System.Data.DataRow row in table.Rows)
  {
    <tr>
      <strong i="9">@foreach</strong> (var col in Columns)
      {
        <td>@row[col]</td>
      }
      <strong i="10">@if</strong> (Select != null)
      {
        <td><a @onclick(() => Select(row))>Seleccionar</a></td>
      }
    </tr>
  }
</table>

@functions{
    public string ClassName { get; set; }
    public List<string> Columns { get; set; }
    public object DataSource { get; set; }
    private System.Data.DataTable table;


    private string Log;

    //call backs it seens I will have many of them

    public Action Change { get; set; }
    public Action<object> Select { get; set; }

    protected void UpdateState()
    {
      int count = ((IEnumerable<object>)DataSource).Count();
      Log = $"Table count rows:{table.Rows.Count} count object ${count}";
      StateHasChanged();
    }

    protected override void OnInit()
    {
      base.OnInit();

      Change += StateHasChanged;

      var enumerable = (IEnumerable<object>)DataSource;
      table = new System.Data.DataTable();

      if (Columns == null)
      {
        Columns = new List<string>();
        Columns.AddRange(enumerable.FirstOrDefault().GetType().GetProperties().Select(x => x.Name).ToList());
      }


      Columns.ForEach(x =>
      {
        table.Columns.Add(x);
      });

      enumerable.ToList()
                .ForEach(x =>
                {
                  var row = table.NewRow();

                  foreach (var property in x.GetType().GetProperties())
                  {
                    row[property.Name] = property.GetValue(x);
                  }

                  table.Rows.Add(row);
                });

      StateHasChanged();
    }
}

@SteveSandersonMS , sollen Title und RowTemplate in Ihrem Beispiel Eigenschaften von MyListWithHeader oder spezielle Komponenten sein? Wenn es sich um Eigenschaften handelt, lohnt es sich möglicherweise, über alternative Möglichkeiten nachzudenken, dies auszudrücken...

Ich arbeite viel mit XAML und als ich mit Blazor anfing, Prototypen zu erstellen, verspürte ich schnell das Bedürfnis nach dieser Funktion. Glauben Sie, dass die Syntax der MyListWithHeader untergeordnete Komponenten hinzuzufügen. Beispiel:

<MyListWithHeader>
    <MyListWithHeader.Title>
       ...
    </MyListWithHeader.Title>
    <MyListWithHeader.RowTemplate>
       ...
    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

Meine persönliche Meinung ist, dass Razor so nah wie möglich an HTML-Idiomen bleiben sollte, anstatt zu den Idiomen abzudriften, die wir in XAML sehen. Das würde bedeuten, dass Sie eher wie die Hierarchie der Elemente aussehen, die Sie in <table> und nicht wie typische XAML-Dinge. Wir könnten generisch genug sein, um beides zu unterstützen, aber ich bin im Moment immer noch sehr fest im Lager von "Razor ist HTML".

voll und ganz dafür. Wenn wir die Liste des Elements "ObservableCollection" anstelle eines "IEnumerable" erstellen können, wäre das ein Bonus. Der Wechsel zu xaml wie pattrens gibt uns ein richtiges Vorlagensystem, das HTML fehlt. Alle gängigen Frameworks in der Web-UI haben ein Vorlagensystem, das sie sehr erweiterbar macht Es wäre großartig, eine zu haben, die bereits von MS und vielen .Net-Entwicklern geliebt wird. Technisch wird sie immer noch auf HTML/CSS basieren, so dass alle HTML-Entwickler immer noch von der Technologie angezogen werden, da sie nicht xaml ist (weiß nicht, warum HTML? guyx hasst xaml ), aber es wird nur xaml-mustern folgen.

@DamianEdwards Ich stimme Ihnen in Bezug auf "Razor ist HTML" voll und ganz zu. Attributwerte in HTML werden jedoch immer als Attribute im öffnenden Tag angegeben. Es gibt keine Möglichkeit (und keine Notwendigkeit), komplexe Attributwerte wie Vorlagen als untergeordnete Elemente auszudrücken. Du kannst zum Beispiel nicht schreiben

<a>
    <href>http://dot.net</href>
</a>

Also denke ich, dass das, was wir technisch anstreben, einfach nicht in reinem HTML ausgedrückt werden kann.
Hier ist eine Zusammenfassung der Optionen, die ich derzeit sehe:

  1. Bleiben Sie bei der HTML-Syntax : Eigenschaftswerte können nur im öffnenden Tag einer Komponente angegeben werden.

    • Ich denke , dafür hat sich React entschieden, zum Beispiel:
      <MyListWithHeader RowTemplate={(data) => <p>...</p>} />
    • Pro : Sehr nahe an normaler HTML-Syntax
    • Con : Wird bei mehrzeiligen Werten unübersichtlich, dh komplexere Vorlagen
  2. Bleiben Sie bei der HTML-Syntax : Abhängig vom Kontext können untergeordnete Knoten einer Komponente entweder verschachtelte Elemente oder Eigenschaftszuweisungen darstellen.

    • Pro : Sehr nahe an normaler HTML-Syntax
    • Nachteil : Könnte verwirrend sein, da Eigenschaftszuweisungen und untergeordnete Elemente syntaktisch nicht zu unterscheiden sind. Im Beispiel könnte <Title>...</Title> eine eigenständige Komponente sein, während <RowTemplate>...</RowTemplate> eine Eigenschaft (ein Attribut) der umgebenden <MyListWithHeader> - ohne IntelliSense wissen Sie es nicht wirklich
    • Nachteil : Kann Namenskonflikte verursachen, wenn eine Komponente und eine Eigenschaft mit demselben Namen vorhanden sind
  3. Neue Syntax erfinden : Die <strong i="39">@functions</strong> { } oder etwas ganz anderes sein.

    • Pro : Erlaubt beliebig komplexe Vorlagen und ist dennoch lesbar
    • Con: Könnte Web - Entwickler entfremden

Es ist durchaus möglich, dass ich hier etwas falsch verstehe. Wenn ja, lass es mich wissen.

@SvenEV Ich glaube nicht, dass die Xaml-Syntax jemanden verfremden wird, wenn Sie bemerken, dass die Vorlagensyntax für Angular-, Vue-, React- und Millionen andere Frameworks alle eine andere Art und Weise und eine andere Syntax haben, um Dinge zu tun, trotzdem werden alle von ihnen geliebt, NEIN? Ebenso kann Blazor seine eigene Syntax haben, der einzige Vorteil in diesem Fall wird sein, dass wir keine neue Syntax erfinden werden, nur die bereits populäre Syntax in dieses Framework übertragen (daher weniger Lernkurve für Xaml-Entwickler), aber was Web-Entwickler betrifft, sie wird sowieso Blazor-Vorlagen lernen, egal ob Blazor xaml folgt oder nicht, die Lernkurve für Webentwickler bleibt gleich Vorlage Modell. :)

Eine völlig neue Template-Syntax bietet also jedem Entwickler eine Lernkurve. aber eine xaml-ähnliche Syntax löst zumindest das Problem von halben Entwicklern. aber xaml ist auch html sehr ähnlich, beide sind schließlich Markup. Sobald Sie es gelernt haben, wird es ein Kinderspiel, das Gute an Xaml ist, Sie müssen sich um CSS überhaupt keine Sorgen machen, das gesamte Styling erfolgt mit direkten Eigenschaften auf den Steuerelementen. und sogar komplettes Styling mit der gleichen Syntax, aber auf der anderen Seite sind HTML und CSS völlig unterschiedliche Sprachen mit unterschiedlicher Syntax und Struktur der Dinge.

Aber warum brauchen wir überhaupt Vorlagen? Ich meine, wenn du direkt in Razor foreach machen kannst. Scheint mir viel einfacher als Vorlagen zu sein.
Oder liegt das an der Wiederverwendung? Ich kann mir eine separate Komponente vorstellen, die in diesem Fall Foreach tut.

@MihaMarkic- Vorlagen standardisieren alles, erhöhen die Wiederverwendung und vor allem verstecken sie den gesamten komplexen Code, der für ein Steuerelement geschrieben wurde, und Sie können dieses Wrap-Steuerelement einfach mit 2 Codezeilen verwenden und sich mehr auf Ihre Geschäftslogik konzentrieren, anstatt sie neu zu erfinden das Rad.

Nun, davon weiß ich nichts. IMO foreach ist eine Vorlage, nur eine leistungsfähigere, und Sie können eine "foreach" -Vorlage in einer anderen Komponente platzieren und sie von überall mit diesen beiden Codezeilen aufrufen.

foreach ist nur eine Schleife und ja, für ein einfacheres Steuerelement können wir dies tun, wie für ein einfaches ListView ( table ) können wir einfach ein Steuerelement erstellen, das einige Eingaben entgegennimmt, das foreachon es ausführt und eine Tabelle ausspuckt, und dann brauchen wir nur noch ein Tag setzenso etwas und dann können wir eine Tabelle erstellen, auch wenn wir eine benutzerdefinierte Listenansicht wünschen, mit sehr modernem Design, sehr interaktiv usw von Blazor-Code in einer Datei, vielleicht mehreren Dateien, und packen Sie all das in eine Komponente und verwenden Sie dann wieder alles nur mit 1 Tag. Siehst du, wohin ich damit komme? Egal wie viel Code ein Steuerelement hat, all das kann in 1 Tag verpackt werden :) @MihaMarkic

Es geht nicht nur darum, mehrere Elemente mit derselben Vorlage zu rendern. Es wird auch benötigt, um mehrere Teile einer Komponente anpassbar zu machen. Derzeit kann eine Komponente nur ein solches "Loch" in ihrem Layout haben, das mit ChildContent gefüllt werden kann, einer vordefinierten Eigenschaft, wie folgt:

<div style="some fancy styling">
    <strong i="7">@ChildContent</strong>
</div>

Stellen Sie sich nun vor, Sie möchten eine Komponente mit mehreren solchen "Löchern" schreiben, z kann sein. Es wäre also schön, wenn Sie so etwas schreiben könnten (wie auch immer die Syntax dann sein wird):

<MyButton>
    <h2>I am the button caption (the usual ChildContent)</h2>

    <MyButton.ContextMenu>
        <button>Context menu button 1</button>
        <button>Context menu button 2</button>
    </MyButton.ContextMenu>

    <MyButton.ToolTip>
        This explains <strong>what the button does</strong>. It is displayed when hovering over the button.
    </MyButton.ToolTip>
</MyButton>

@SvenEV Hat das nicht nur mehrere Löcher und erfordert nicht unbedingt eine Vorlage?

Ja, das ist ein gutes Beispiel für einen anderen Anwendungsfall, jeder Teil sollte anpassbar sein.

@MihaMarkic Es stimmt, wir sprechen hier von zwei verschiedenen Dingen:

  1. Unterstützung für mehrere Löcher ( RenderFragment s / Action<RenderTreeBuilder> ) wie in meinem Beispiel
  2. Unterstützung für Vorlagen mit Dateneingabe ( Action<RenderTreeBuilder, T> ) wie in ListView usw.

Ich denke, ersteres ist Voraussetzung für das zweite und ich hoffe, dass beide Features irgendwann implementiert werden. Dies würde es uns ermöglichen, einige leistungsstarke Komponentenbibliotheken zu erstellen.

Welches Problem wird damit versucht zu lösen? Ich frage mich nur, da ich so etwas in anderen SPA-Frameworks noch nicht gesehen habe.

Steht es im Widerspruch zu einer komponentenbasierten Struktur? Verstößt es auch gegen normale Webentwicklungspraktiken, nur eine "App-ähnliche" Struktur zu erzwingen?

@AWulkan es verstößt nicht gegen die normale Komponentenstruktur, aber ja, es geht leicht in Richtung App-Dev-ähnlicher Struktur, aber es bedeutet nicht, dass es von der Webentwicklung weggeht des SPA-Konzepts war dies. SPA wurde geschaffen, um die Webentwicklung einer App-Entwicklung ähnlicher zu machen. und jetzt folgt jeder rahmen spa. der nächste schritt von spa ist jetzt pwa. Weißt du, was ein PWA richtig ist? Es führt einige wichtige Funktionen von nativen Apps wie Benachrichtigungen und Offline-Synchronisierung ein, sodass die Webentwicklung als Ganzes versucht, der nativen App-Entwicklung zu folgen.

@AWulkan Da Sie mit Vue vertraut zu sein scheinen, habe ich gerade in den Vue-Dokumenten nachgesehen, ob es etwas Ähnliches gibt. Es gibt und heißt „ Slots “, insbesondere „Named Slots“ und „Scoped Slots“ – genau darum geht es in dieser Ausgabe. Es ist das gleiche Konzept. Vielleicht hilft dir das, das Problem zu verstehen?

@SvenEV Okey Ich verstehe dann, was du meinst. Für mich ein ziemliches Nischenfeature. Danke fürs klarstellen.

Wird es diesen Entwurf als Modell verwenden?
https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md

Scheint, dass Vue.js-Slots der XAML-Datenvorlage ähnlich sind.
Templating bietet eine leistungsstarke Möglichkeit, vorhandene Komponenten anzupassen. Ein Fall, den ich sehen konnte, ist eine ListView, bei der standardmäßig ohne Templating nur eine Liste von Strings durchlaufen und angezeigt wird.

<ListView list=<strong i="7">@ListOfUrlsAsStrings</strong> />

Mit Templating können Sie dies überschreiben, indem Sie stattdessen eine Liste von Image-Tags mit dem übergebenen Type T .

<ListView list=@ListOfUrlsAsStrings>
    <ListView.ListTemplate>
         <img src=<strong i="12">@item</strong> alt="overriden by a template" />
        <CustomComponentHere />
    </ListView.ListTemplate>
</ListView>

Dies würde die Wiederverwendbarkeit von Komponenten stark erhöhen, da sie dynamisch sein können. Drittanbieter von Komponenten können auf diese Weise beim Erstellen von Komponenten kreativer sein.

ja genau mit Datenvorlagenbindung wird einfacher und beobachtbarer

Wir hoffen sehr, dass Blazor-Komponenten eine gute Unterstützung für Nesting und Templating bekommen.
Zuvor hatten wir eine Reihe von Diskussionen mit @Mehul , @NTaylorMullen , @DamianEdwards zum gleichen Thema für Razor Tag Helpers, in denen wir versuchten zu erklären, warum wir einen XAML-ähnlichen Ansatz benötigen. Relevante GitHub-Probleme: aspnet/Razor#474, ​​aspnet/Razor#576, aspnet/Razor#570.

Wir entwickeln UI-Komponenten wie Datengrids und das typische Markup zum Konfigurieren eines Widgets kann so zwiespältig sein:

<TabPanel>
    <Grid>
        <DataSource>
            <More about data source />
        </DataSource>

        <Column />
        <Column>
            <Lookup>
                <DataSource>
            . . .
            <CellTemplate>
                arbitrary html
            </CellTemplate>
        </Column>

        <DetailTemplate>
            <div>arbitrary content</div>
            <ThirdPartyComponent />

            <Grid>
                detail grid configuration with own templates (and there may be more levels)
            </Grid>
        </DetailTemplate>
    </Grid>
</TabPanel>

Für eine solche Hierarchie möchte ein Entwickler eine genaue Kontexterkennung und Intelligenz haben.

@AlekseyMartynov können wir auf lange Sicht auch xaml-ähnliche Styling + Animationen haben?

Ich freue mich, dass diese Idee Anklang findet. Da MS mit Silverlight, WPF und UWP Pionierarbeit bei der Idee von vorlagenfähigen Steuerelementen geleistet hat, ist es sinnvoll, dies zu einem Teil von Blazor zu machen.

Eine Frage wären Daten-/Code-Kontexte. Wenn die cshtml-Vorlage auf Code im gebundenen C#-Objekt verweist, befindet sich dieser wahrscheinlich nicht im selben Kontext wie die größere Komponente. Wäre also nicht eine Art dynamischer Datenbindung erforderlich? Oder hätte eine Vorlage eine strikte TargetType-Anforderung, damit der Parser weiß, wie Verweise auf datengebundene Objektmember aufgelöst werden?

Wie sieht es in diesem Zusammenhang mit dynamischen Datenbindungs- und Abhängigkeitseigenschaften aus? Soweit ich das beurteilen kann, steht dem jetzt nichts mehr im Wege. Oder wird die Reflexion problematisch, wenn C# oder IL zu Wasm kompiliert werden, anstatt von Mono interpretiert zu werden?

Die Umsetzung dessen, was @AlekseyMartynov vorschlägt (oder eine Art Variation), ist meiner Meinung nach entscheidend für den Erfolg von Blazor. In der Praxis sind viele Entwickler, die Unternehmensanwendungen schreiben, auf eine Vielzahl einfach zu verwendender Komponenten von Drittanbietern angewiesen, und ich hoffe, dass dieses Projekt sehr bald an dem Punkt sein wird, an dem verschiedene Anbieter mit der Entwicklung von Blazor-Komponenten beginnen können.

In meinem Projekt Xamzor habe ich einige eingeschränkte Vorlagenunterstützung wie

<ListView ItemTemplate="typeof(MyCustomTemplate)" ... />

wobei MyCustomTemplate eine weitere Blazor-Komponente mit einer Eigenschaft "DataContext" ist, die von der ListView zugewiesen wird. Obwohl dies recht gut funktioniert, ist es dennoch wünschenswert, Vorlagen inline statt in einer separaten Komponente zu definieren, insbesondere wenn die Vorlagen klein und einfach sind.

Vielleicht können kontextbezogene Komponenten von ember.js ein wenig Inspiration bieten: https://www.mutuallyhuman.com/blog/2016/09/23/creating-an-accordion-with-contextual-components-in-ember

@SvenEV Ich stimme zu, dass im Falle einfacher Vorlagen Inline-Vorlagen hilfreich wären, aber wir sollten immer noch die Option für separate Komponentenvorlagen haben, wie Sie sie in unserem Projekt implementiert haben, da dies uns bei der Trennung von Bedenken und der Wiederverwendbarkeit der Vorlage hilft :)

Drei Dinge in meinem Kopf:

  1. Machen Sie Komponenten nativ hierarchisch (z. B. ParentComponentType-Eigenschaft; damit Sie MyTableComponent -> MyRowComponent -> MyColumnComponent erzwingen können). Wenn ParentComponentType null ist, kann die Komponente ein untergeordnetes Element eines beliebigen Komponententyps sein (z. B. MyTableComponent.ParentComponentType kann null sein).

Die Hierarchie wird durch Deklarationen in den Definitionen der Komponenten wie folgt durchgesetzt:

```C#
// In MyRowComponent
Typ ParentComponentType { get; } = typeof(MyTableComponent);
...

// In MyColumnComponent
Type ParentComponentType {get; } = typeof(MyRowComponent); 
...
Thus, MyColumnComponent can only be created within a MyRowComponent, and MyRowComponent can only be created within a MyTableComponent.

2. Allow components to be extended via ITemplateable interface. When the ITemplateable.Template [Tag] property is valued, it overrides the component's default rendering. Internally, it's something like:

```C#
    if (component is ITemplateable)
    {
        if (component.Template != null)
        {
            … return template rendering ...
        }
    }
    … return default rendering ...
  1. Erstellen Sie eine ITemplate-Schnittstelle, die als optionale Basisklasse BlazorTemplate implementiert werden kann, die verwendet werden kann, um die Programmierung mit Vorlagen zu objektivieren.

Beispielverwendung, bei der MyColumnComponent ITemplateable ist:

    <MyTableComponent>  
        <MyRowComponent>  
            <MyColumnComponent>  
                <Template>  
                    …  template codes here; can contain a call to another component ...
                </Template>  
            </MyColumnComponent>  
        </MyRowComponent>  
    </MyTableComponent>  

Wenn MyColumnComponent als ITemplateable definiert ist, unterstützt es das Enthalten eines <Template> … </Template> Tags. Die Einträge in diesem Tag überschreiben das Standard-Rendering von MyColumnComponent.

Intern kann MyColumnComponent eine BlazorTemplate-Implementierung haben/verwenden, damit spezielle Vorlagenverhalten, Ereignisse und Funktionen programmgesteuert verarbeitet werden können.

Ich stimme für Property Element Syntax

<Button>
  <Button.ContextMenu>
    <ContextMenu>
      <MenuItem Header="1">First item</MenuItem>
      <MenuItem Header="2">Second item</MenuItem>
    </ContextMenu>
  </Button.ContextMenu>

  Right-click me!
</Button>

Als Alternative zu

<Button ContextMenu=@GetContextMenuRenderFragment()>
  Right-click me!
</Button>

Ich mag es, aber würden die Web-Puristen nicht die Nase rümpfen, weil sie XAML zu sehr ähnlich sind?

Nein, sie werden in Ordnung sein, property element syntax ist eine Alternative zu html attribute syntax (kein Ersatz). Aber ich beende, wie viele Möglichkeiten es für Komponentenentwickler bietet (UI steuert Bibliotheksentwickler)

Ich mag es auch, da es deutlicher vermittelt, wie das Kind mit dem Elternteil in Beziehung steht. Dies ist besonders gut bei Templating, da Sie in der Lage wären, zu unterscheiden, dass es sich um eine Datenvorlage von einem Elternteil handelt und nicht um eine benutzerdefinierte Komponente, die keine Beziehung zu . hat die Elternkomponente.

So was:

<List>
    <List.DataTemplate>
      <!-- Template Here -->
    </List.DataTemplate>
    <ComponentWithNoRelationToTheList />
</List>

Dies gibt Ihnen eine Vorstellung davon, dass DataTemplate mit List zusammenhängt als eine benutzerdefinierte Vorlage insgesamt. Obwohl ich wie XAML zustimme, sollte es nur eine Alternative sein.

Was ist, wenn "Vorlage" ein reserviertes/spezielles Schlüsselwort für eine vorlagenfähige Blazor-Komponente ist? Wenn es als Tag verwendet wird, kann es die Vorlagencodes enthalten. Wenn es als Attribut verwendet wird, kann es eine benannte Vorlage angeben, die die Vorlagencodes enthalten kann.

Stellen wir uns also eine Komponente vor wie:

ComponentTemplateTest.cshtml

<strong i="7">@functions</strong>
{
  [Parameter]
  public RenderFragment Template { get; set; }

  [Parameter]
  public RenderFragment<int> TemplateWithInt { get; set; }

  [Parameter]
  public RenderFragment<TestObject> TemplateWithObject { get; set; }

  public class TestObject
  {
      public string PropString { get; set; }
      public int PropInt { get; set; }
  }

  private TestObject testobject = new TestObject();
}

<div>
    <div><button onclick="@((x) => this.testobject.PropInt++)">test</button></div>
    <div>@Template</div>
    <div>@((RenderFragment)(x => TemplateWithInt(x, testobject.PropInt)))</div>
    <div>@((RenderFragment)(x => TemplateWithObject(x, testobject)))</div>
</div>

wir können es verwenden als

<ComponentTemplateTest>
    <Template>
        test simple template
    </Template>

    <TemplateWithInt params="param1">
        <strong i="11">@param1</strong>
    </TemplateWithInt>

    <TemplateWithObject params="paramObject">
        @paramObject.PropInt
    </TemplateWithObject>
</ComponentTemplateTest>

wir brauchen ein Attribut wie params weil Blazor den Code schreibt als

            builder.OpenComponent<StandaloneApp.ComponentTemplateTest>(16);
            builder.AddAttribute(17, "template", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
                builder2.AddContent(18, "\n        test simple template\n    ");
            }
            ));
            builder.AddAttribute(19, "templatewithint", (Microsoft.AspNetCore.Blazor.RenderFragment<System.Int32>)((builder2, param1) => {
                builder2.AddContent(20, param1);
            }
            ));
            builder.AddAttribute(21, "templatewithobject", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.ComponentTemplateTest.TestObject>)((builder2, paramObject) => {
                builder2.AddContent(22, "\n        ");
                builder2.AddContent(23, paramObject.PropInt);
                builder2.AddContent(24, "\n    ");
            }
            ));
            builder.CloseComponent();

würde es reichen? etwas fehlt?

@uazo Beachten Sie nur, dass es ohne Property Element Syntax unmöglich ist, zwischen property und component .

@danieldegtyarev Ich denke, dass wir hier über zwei verschiedene Dinge sprechen ... mein Ziel ist

schau mal auf https://github.com/uazo/Blazor/tree/dev-experimental-template-component
Sie müssen diese Erweiterung erstellen und installieren ...

Das als Tag verwendete "Template" ist deklarativ und natürlich inline:

<MyCom MyComProp="MyComPropValue">
    <Template>
        Components, @MyCom.MyComProp, HTML, content and <strong i="6">@values</strong>
    </Template>
</MyCom>

"Vorlage" wird als Tag verwendet, um eine "benannte Vorlage" zuzuweisen. Intern hat die benannte Vorlage Zugriff auf ihre übergeordnete Komponente, sodass auch etwas wie MyCom.MyComProp möglich ist:

<MyCom MyComProp="MyComPropValue">
    <Template Name="MyTemplate {Content: @content; Values: @values}" />
</MyCom>

"Vorlage" wird als Attribut verwendet, um eine "benannte Vorlage" zuzuweisen. Intern hat die benannte Vorlage Zugriff auf ihre übergeordnete Komponente, sodass auch etwas wie MyCom.MyComProp möglich ist:

<MyCom 
    MyComProp="MyComPropValue"
    Template="MyTemplate {Content: @content; Values: @values}" />

Die entweder als Tag oder als Attribut verwendete "Vorlage" sollte zu derselben Zusammenstellung führen, sodass die Komponente die bereitgestellten Vorlageninformationen rendern würde, die einfach das Standardrendering der Komponente überschreiben.

Eine "benannte Vorlage" ist eine wiederverwendbare Vorlage. Das Erstellen einer "benannten Vorlage" ist fast wie das Erstellen einer Komponente. Der große Unterschied besteht jedoch darin, dass eine benannte Vorlage nur durch die Unterstützung einer Komponente für "Vorlage" verwendet werden kann.

Eine Vorlage kann Komponenten enthalten, aber es müssen Überprüfungen eingefügt werden, um potenzielle Endlosschleifen zu verhindern, wenn beispielsweise eine Komponente codiert wird, die eine Vorlage verwendet, die dieselbe Komponente verwendet ... vielleicht ein Fehler beim Entwurf/der Kompilierung?

Fragen: Kann eine benannte Vorlage von einer anderen Vorlage erben? Kann eine benannte Vorlage auf die Verwendung mit bestimmten Komponententypen beschränkt werden?

Ich denke, ihr solltet so viel wie möglich auf die HTML-Syntax setzen, keine neuen Sachen einführen. Keine Xamarin-XAML-Syntax oder hässliche asp.net-Webforms-Komponenten (ASP-Repeater usw.).
Ich komme aus der asp.net mvc-Welt, ich erinnere mich an die Freude und Freiheit, als ich von "asp.net webforms" zu "asp.net mvc" wechselte, keine seltsamen Komponenten mehr wie asp:repeater und so weiter.
Bitte lassen Sie BLAZOR nicht zu einem neuen "asp.net webforms" werden.

Ich stimme @GoranHalvarsson zu . Das Erstellen von Vorlagen in Blazor sollte nur die Implementierung von generischem RenderFragment sein, mit gleichem Anwendungsfall (erstes Beispiel im @etmendz- Post) .

Dafür habe ich meinen Branch aktualisiert, jetzt mit voller Unterstützung für Intellisense in Visual Studio (Params und Allowed Child).

kann mir jemand ein Feedback geben?

@ GoranHalvarsson Ich stimme zu. Das MVP kann für Blazor-Komponenten sein, um grundlegende Vorlagenunterstützung zu haben. In diesem Sinne kann eine "Vorlage" HTML-, Komponenten-, Inhalts- und Razor-Codes enthalten. Im Grunde sollten Blazor-Komponenten+Vorlagen immer noch erkennbar HTML+Razor-Codes sein. Dazu passt die deklarative Syntax, die beispielsweise das Schlüsselwort <Template> oder @template kann.

Ein Element der nächsten Phase können die APIs/Erweiterbarkeitsbibliotheken von Blazor sein, die es den Entwicklern von Komponenten ermöglichen, ihre eigene Marke zu bewerben – z .Uno) können stattdessen Lieferungen auf Drittanbieter-/Lieferantenebene sein. Innerhalb von Blazor kann an der Programmierbarkeit von Vorlagen und an der Kompilierungsflexibilität eng mit den Komponentenersteller-Partnern gearbeitet werden, um spezifische Probleme/Anforderungen/Anforderungen zu erfüllen.

In diesem Thread gibt es einen Vorschlag zu wiederverwendbaren Vorlagen. Wiederverwendbare Vorlagen oder "benannte Vorlagen" können die Wiederverwendung von Code fördern. Obwohl ich ehrlich gesagt keinen großen Wert darin sehe und eine niedrige Priorität haben kann. Wahrscheinlicher, wenn es für die Wiederverwendung gedacht ist, dann sollte es vielleicht eine Komponente sein? ;-)

@uazo IMHO, basierend auf Ihren Beispielen, scheint die Übergabe von Parametern an <Template> die deklarative Natur der einfachen Verwendung des Parameters / Werts direkt innerhalb des <Template> Tags zu überwinden.

Ich habe auch Zweifel, ob Komponenten mehrere Vorlageneigenschaften unterstützen. In diesem Sinne macht es auch Freestyle. Ich denke, Blazor sollte eine einfachere und standardmäßigere Möglichkeit zur Unterstützung von Komponentenvorlagen haben, vielleicht über die ITemplate-Schnittstelle und einige Compiler-Optimierungen hier und da.

@SteveSandersonMS für https://github.com/aspnet/Blazor/issues/404#issuecomment -377229910
mit aspnet/AspNetCore#15829 Code wird

MyListWithHeader.cshtml

<strong i="10">@functions</strong>
{
    [Parameter]
    public RenderFragment Title { get; set; }

// NOT MANDATORY
    [Parameter]
    public RenderFragment ChildContent { get; set; }
// *******************

    [Parameter]
    public IEnumerable<example.Person> RowItems { get; set; }

    [Parameter]
    public RenderFragment<example.Person> RowTemplate { get; set; }
}

<div>
    <strong i="11">@Title</strong>
</div>

<strong i="12">@foreach</strong> (var item in RowItems)
{
    <div>
    @RowTemplate.Render(item)
    </div>
}

<strong i="13">@ChildContent</strong>

beispiel.cshtml

<strong i="17">@page</strong> "/component-test"

<MyListWithHeader RowItems=@People>
    <MyListWithHeader.Title>
        Here I'm still in the outer context so can reference properties on the component itself
        such as <strong i="18">@TitleText</strong>
    </MyListWithHeader.Title>

    <MyListWithHeader.RowTemplate WithParams="item">
        <!-- Somehow need intellisense to know that RowTemplate is meaningful here, and that its "item" value is taken from the generic type arg on the RowItems property -->
        Here I'm in the context of an Action RenderTreeBuilder, Person
        because we supplied
        an IEnumerable Person as the value for RowItems, so I can write <b>@item.FirstName</b>
    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

<strong i="19">@functions</strong>
{
  string TitleText { get; set; }
  IEnumerable<Person> People { get; set; }

  protected override void OnInit()
  {
      var people = new List<Person>();
      people.Add(new Person { FirstName = "uno" });
      people.Add(new Person { FirstName = "due" });
      people.Add(new Person { FirstName = "tre" });

      this.People = people;
  }

  public class Person
  {
      public string FirstName { get; set; }
  }
}

mit voller Unterstützung für Intellisense!

@uazo Sehr sehr gut! Es gibt nur eine Sache: Lass uns WithParams="item" entfernen und einfach @item um auf den 'Kontext' der Vorlage zuzugreifen

ähm .. aber wenn item bereits in der Komponente example ?
Da der Variablenname, der für den Zugriff auf den 'Kontext' der Vorlage verwendet wird, nicht erkannt werden kann, können Entwickler mit WithParams den Namen der Variablen bestimmen.

Denken Sie an einen kabelgebundenen Standardnamen (wie ChildContent ), mit oder ohne optionales WithParams ? Oder eine Sintax wie <MyListWithHeader.RowTemplate WithParams=@item> ?

Rückblickend wäre ein Standardname für verschachtelte Vorlagen nicht in Ordnung...

Denken wir nach. Template ist Codegenerierung mit einem Parameter. Vorlage hat nur Zugriff auf dieses Element. Wenn wir also eine Variable item 'reservieren' - ist sie sicher und kann nicht kollidieren. Verschachtelte Vorlagen haben ihren eigenen Geltungsbereich und die Verwendung von @item ist weiterhin sicher.

Übrigens, sieht aus wie diese Vorlage - scope müssen auch ihren @functions Block haben

Lass uns

<MyListWithHeader RowItems=@People>
    <MyListWithHeader.Title>
        ...<strong i="6">@TitleText</strong>
    </MyListWithHeader.Title>

    <MyListWithHeader.RowTemplate WithParams="item">
        ... <b>@item.FirstName</b>

        <MyListWithHeader>
            <MyListWithHeader.RowTemplate WithParams="item">
                @item.FirstName
            </MyListWithHeader.RowTemplate>
        </MyListWithHeader>

    </MyListWithHeader.RowTemplate>
</MyListWithHeader>

wird Renderer sein als

protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
    base.BuildRenderTree(builder);
    builder.OpenComponent<StandaloneApp.Pages.testtemplated.MyListWithHeader>(0);
    builder.AddAttribute(1, "RowItems", Microsoft.AspNetCore.Blazor.Components.RuntimeHelpers.TypeCheck<System.Collections.Generic.IEnumerable<StandaloneApp.Pages.testtemplated.example.Person>>(People));
    builder.AddAttribute(2, "Title", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
        builder2.AddContent(3, "\n        Here I\'m still in the outer context so can reference properties on the component itself\n        such as ");
        builder2.AddContent(4, TitleText);
        builder2.AddContent(5, "\n    ");
    }
    ));
    builder.AddAttribute(6, "RowTemplate", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.Pages.testtemplated.example.Person>)((builder2, item) => {
        builder2.AddContent(7, "\n        ");
        builder2.AddContent(8, "\n        Here I\'m in the context of an Action RenderTreeBuilder, Person\n        because we supplied\n        an IEnumerable Person as the value for RowItems, so I can write ");
        builder2.OpenElement(9, "b");
        builder2.AddContent(10, item.FirstName);
        builder2.CloseElement();
        builder2.AddContent(11, "\n\n        ");
        builder2.OpenComponent<StandaloneApp.Pages.testtemplated.MyListWithHeader>(12);
// line 43
        builder2.AddAttribute(13, "RowTemplate", (Microsoft.AspNetCore.Blazor.RenderFragment<StandaloneApp.Pages.testtemplated.example.Person>)((builder3, item) => {
            builder3.AddContent(14, "\n                ");
            builder3.AddContent(15, item.FirstName);
            builder3.AddContent(16, "\n            ");
        }
        ));
        builder2.CloseComponent();
        builder2.AddContent(17, "\n\n    ");
    }
    ));
    builder.CloseComponent();
}

mit

Error   CS0136  A local or parameter named 'item' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter

StandaloneApp\obj\Debug\netstandard2.0\Pages\testtemplated\example.g.cs line 43

Ja, klar, Sie müssen es während des Codegens auflösen.
Aber... ich denke, dass Template zu component (möglicherweise eine spezielle Komponente) kompiliert werden muss und nicht zu RenderFragment weil wir mit seinem Zustand umgehen müssen, zum Beispiel um StateHasChanged() aufzurufen, wenn @item.FirstName mutiert.

Ja, klar, Sie müssen es während des Codegens auflösen.

Ich glaube nicht, dass es möglich ist.
Während des Codegens (das ist rasiermesserscharf modifiziert) werden alle Knoten gekreuzt, einige sind html, andere sind c#-Knoten: die mit c# wären falsch, weil die Variablen nicht definiert wären und daher der Variablentyp nicht bekannt wäre (und wir müssen uns wiederherstellen eine Variable vom Typ T von RenderFragment<T> , weil wir T ) ... also weiß ich nicht, wie die Variable heißt.
Sie könnten wahrscheinlich nach dieser undefinierten Variablen suchen, aber es scheint ein Durcheinander zu sein

Aber ... Ich denke, die Vorlage muss zu einer Komponente (möglicherweise zu einer speziellen Komponente) kompiliert werden und nicht zu RenderFragment, da wir mit ihrem Zustand umgehen müssen, beispielsweise um StateHasChanged() aufzurufen, wenn @item.FirstName mutiert.

Ich glaube nicht, dass es notwendig ist, etwas anderes einzuführen.
Wenn Sie eine Komponente benötigen, verwenden Sie BlazorComponent , wenn Sie eine "Vorlageneigenschaft" benötigen, verwenden Sie RenderFragment<T>
Schließlich wird StateHasChanged() für ein RenderFragment von StateHasChanged() der zugrunde liegenden Komponente aufgerufen, ich muss das Aufrufen von Änderungen nicht auf andere Weise erzwingen

Es tut mir leid, dass ich mich hier einmischen muss, weil es sich nach einer großartigen Konversation anhört - aber -

Würde dies nicht mit einem Datenkontextkonzept wie WPF/UWP gelöst werden? Dann würden Sie in Ihrer Vorlage immer auf this.DataContext.[Member] verweisen.

@peter0302 Ich persönlich mag diese Idee ...

Die Herausforderung bleibt stark typisiert. Dies wird in XAML natürlich dadurch behoben, dass die Bindung zur Laufzeit erfolgt. Die Datenvorlage hier müsste wahrscheinlich die Eingabe durch Generika unterstützen. Aber definitiv machbar.

Andererseits würde ich auch einen vollständig dynamischen Laufzeitbindungsmechanismus unterstützen.

Sie könnten den Datentyp irgendwie per Deklaration angeben. Etwas ähnlich wie x:DataType in XAML, obwohl die Ableitung idealer und weniger fehleranfällig sein sollte.

@uazo Mein Feedback ist, dass es sehr gut aussieht! Hier meine Gedanken...

1) Dass einer Eigenschaft vom Typ RenderFragment ein qualifizierter Name einer Eigenschaft in einem Tag wie <MyListWithHeader.Title /> zugewiesen wird, fühlt sich für mich in Ordnung an, aber die Handhabung bricht meine frühere Intuition, dass Eigenschaften mit ParameterAttribute gesetzt sind über HTML-Attribute. Wie wäre es mit einem anderen c#-Attribut für diese Eigenschaften, um dieses Verhalten zu unterscheiden? Wie [ContentParameter] ?

2) Dass einer Eigenschaft vom Typ RenderFragment<Person> ein Eigenschafts-Tag mit Attributen zugewiesen wird
<MyListWithHeader.RowTemplate WithParams="item" /> fühlte sich zuerst etwas seltsam an. Es ist kein Attribut, das eine Parametereigenschaft festlegt, aber es ist der Variablenname, der im Lambda-Ausdruck während der Codegenerierung lokal für diese Vorlage verwendet werden soll. Auch hier wäre es gut, eine andere Syntax zu haben, um den Unterschied zu erkennen. Aber ich bin mir nicht sicher, wie kreativ man sein kann, um mit Tools und Compiler kompatibel zu sein. <MyListWithHeader.RowTemplate params(item) /> vielleicht?

3) Wenn ich <MyListWithHeader RowItems=<strong i="16">@PeopleOnline</strong> /> und <MyListWithHeader RowItems=<strong i="18">@PeopleOffline</strong> /> anzeigen möchte, muss ich dann die Vorlagen erneut angeben? Gibt es eine Möglichkeit, sie wiederzuverwenden? Ich dachte daran, vielleicht eine modellgesteuerte Initialisierung durch diesen Mechanismus durchzuführen, ähnlich wie bei EditorFor mit verschiedenen Eigenschaftsselektoren, die dieselben Vorlagen verwenden.

Es könnte ein bisschen riskant sein, die Implementierung zu diskutieren, wenn ich neu im Framework bin, aber…

Wenn Sie ein separates Parameterattribut für die Zuweisung von Vorlagen verwenden, wie unter Punkt 1 oben beschrieben, können Sie ein Boolesches Flag (Standardwert true) hinzufügen, um anzugeben, ob ein Standardwert (wenn nicht explizit angegeben) angewendet werden soll. Dann können Sie Punkt 3 oben lösen, indem Sie eine Schnittstelle wie IDefaultTemplateProvider einführen, mit der Sie Definitionen für die Template-Property-Tags separat bereitstellen können. Diese können dann über Reflektion auf die gleiche Weise wie IComponent(s) erkannt und in Fällen verwendet werden, in denen das use-default-Flag gesetzt ist und der Benutzer keines bereitstellt. Sie könnten mehrere Vorlagenkomponenten zulassen, die die Schnittstelle implementieren, aber eine eindeutige Ausnahme auslösen, wenn ein Eigenschafts-Tag mehrmals gefunden wird. Auf diese Weise können Sie einer generischen Komponente, die spezifisch für eine App ist, standardisierte Vorlagen bereitstellen, die mehrmals verwendet werden und die Auflösung möglicherweise zur Entwurfszeit erhalten?

Ich denke, die Beschreibung meiner Lernerfahrung in diesem Bereich bietet Ihnen einen guten Hintergrund ... und ich habe einige Gedanken zu besprechen ...

Ich möchte eine modellgesteuerte Konfiguration von Komponenten durch Metadaten und Ressourcendateien zum Abrufen von statischem übersetztem Text, Validierung und dergleichen vornehmen. Ich dachte, dass dieser Mechanismus des Templatings das Richtige sein könnte, um dies zu ermöglichen. Um es auszuwerten, habe ich eine Schnittstelle public interface IRenderFragment<T> { RenderFragment UsingDataItem(T item); } und eine normale Komponente erstellt, die diese Schnittstelle auch als Vorlage implementiert. Dann habe ich eine generische Komponente damit erstellt

  <InputField For=@DataSource(Model, x => x.NewTodo) Template="@InputBasic.Template" />

Auf diese Weise verfügt die InputField-Komponente über Informationen zum Abrufen von Metadaten usw. und kann diese in ein Modell einspeisen, das in einer von Ihnen bereitgestellten Vorlage gerendert wird. Es funktioniert sehr gut.

In XAML ist es der einzige Mechanismus, um Dinge zu rendern, wenn ein umgebendes Verhalten in einer Komponente implementiert ist, wie in einer Listenansicht, dann müssen Sie eine Elementvorlage bereitstellen. In Blazor ist dies nicht notwendig, da Sie stattdessen Code mit einer Schleife verwenden können, um dieses umgebende Verhalten zu haben. Dann wurde mir klar, dass ich hier dasselbe hätte tun können. Ich hätte stattdessen ein Modell aus Metadaten im Code abrufen und dieses in eine normale Komponente einspeisen können. @{ var inputModel = InputModelHelper.GetFor(ViewModel, x => x.NewTodo); } <InputBasic DataItem="@inputModel"/>

Für manche vielleicht offensichtlich… also sind Vorlagen für meinen Anwendungsfall eigentlich nicht unbedingt notwendig. Aber es hat mich gefragt, was ich wirklich will. Vielleicht geht es eher darum, Attribute und Inhalte in bestehende Elemente einzufügen? Und vielleicht tun Sie dies, indem Sie ein benutzerdefiniertes Attribut wie folgt definieren können:

  <label For=”@DataSource(Model, x => x.NewTodo)”></label>
  <input For=”@DataSource(Model, x => x.NewTodo)” />

Um zu testen, ob es überhaupt möglich ist, Attributknoten zu einem vorhandenen Element hinzuzufügen, habe ich nach dem Open-Tag <label>@For(x => x.NewTodo)</label> eine Funktion (gibt einen leeren String für diesen Inhaltsknoten zurück) erstellt, die den Rendering-Prozess um das Hinzufügen von Attributknoten (und einem Inhalt) erweitert Knoten im Fall des Label-Elements) an den Builder. (Der Builder wurde in der Methode BuildRenderTree erfasst.) Der Builder hat die Position des Label-Knotens, sodass man in seinem Puffer leicht sehen kann, ob ein Attribut gesetzt wurde und daher von diesem Mechanismus nicht überschrieben werden sollte. Es funktioniert tatsächlich. Für das input-Element ist es etwas schwieriger, da es ein void-Element ist (es muss erneut geöffnet werden und die Funktion kann nicht zwischen den Attributen platziert werden), aber als Proof-of-Concept scheint es zu funktionieren. Wie kann man dies unterstützen und ist es sinnvoll?

Ich denke, diese Renderfortsetzungs-/Erweiterungsfunktion kann an mehreren Stellen eingehängt werden. Um Sie zum Nachdenken zu bringen ... vielleicht haben Sie die Möglichkeit, einen "RenderElementExtensionHandler" einzufügen, und wenn eine Komponente eine solche Eigenschaft hat, wird ein Aufruf an die Position kompiliert, wenn alle Attribute hinzugefügt wurden, und nach dem Schließen im generierten Code für die Elemente mit benutzerdefinierten Attribute, wie in diesem Fall For.
Ist das etwas, das für andere Dinge verwendet werden kann oder ist dies ein Randfall? Gibt es andere Ideen, das Verhalten auf bestehende Elemente auszudehnen?

Sorry, wenn es etwas langatmig wurde…

Warum nicht Requisiten rendern?

<List RenderHeader={(text) => (<h4>{text}</h4>)}>
    {({ item, index }) => (
        <li value={index} class={item.Type}>
            {item.Content}
        </li>)}
</List>

Fertig:
b2c8b1aec98e65054c590d356fdf15e6a5e6a822
409026c2f41926d85d18ea2abf345453fa083e34
ce11f517d370b501433a62a56fbd4be1704000ad

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen