Taille de la page
Nom complet | Email | Téléphone | Âge | Compte | |
|---|---|---|---|---|---|
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
|
Réglé AllowEdit="true" sur MainGrid / SubGrid et un bouton de bascule étiqueté Éditable apparaît dans le menu d’engrenages en surplus. L’activer permet de changer chaque colonne déclarée dont le moteur de rendu de la cellule n’est pas remplacé dans un éditeur en ligne — qui est automatiquement envoyé depuis les métadonnées Dataverse de la colonne — tandis qu’un GridTemplateColumn's EditChildContent prend le relais de son mode ChildContentlecture. Les modifications s’accumulent en tant que changements de lignes en attente dans le tampon transactionnel de la grille et ne tombent dans Dataverse que lorsque l’utilisateur sauvegarde.
Un MainGrid par-dessus la contact table avec AllowEdit="true". Cliquez sur l’icône d’engrenage dans le débordement de la barre d’outils et activez Modifiable . La colonne Nom complet remplace son modèle initials-avatar read par un éditeur personnalisé à champ unique qui divise les entrées utilisateur en firstname + lastname. Les colonnes nues Email, Téléphone, Âge et Compte s’allument toutes avec l’éditeur correspondant à leur type de métadonnées — TextEdit pour les chaînes, NumberEdit pour l’entier, LookupEdit pour la référence client — sans aucun câblage par colonne.
Taille de la page
Nom complet | Email | Téléphone | Âge | Compte | |
|---|---|---|---|---|---|
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
| |||||
|
|
Réglé AllowEdit="true" sur la grille. Le montage se fait par option par grille — le basculement n’apparaît que lorsque le paramètre est défini, et le basculement lui-même n’est visible que lorsqu’au moins une colonne est modifiable. Lorsque le bouton est désactivé, la grille se comporte comme une vue en lecture seule ; Pendant qu’il est activé, les cellules affichent leurs éditeurs en ligne et les modifications non enregistrées sont suivi par ligne.
<MainGrid tableName="contact" allowEdit>
<GridColumns>
<GridColumn columnName="firstname" />
<GridColumn columnName="lastname" />
<GridColumn columnName="emailaddress1" />
</GridColumns>
</MainGrid><MainGrid TableName="contact" AllowEdit="true">
<GridColumns>
<GridColumn ColumnName="firstname" />
<GridColumn ColumnName="lastname" />
<GridColumn ColumnName="emailaddress1" />
</GridColumns>
</MainGrid>Pourquoi un basculement ?
L’édition en ligne modifie le rendu des cellules pour chaque ligne visible, ce qui a une capacité de lecture différente d’une grille statique. Le toggle permet aux utilisateurs d’opter pour la possibilité d’édition quand ils en ont besoin (taper une valeur, faire glisser des onglets entre les cellules, etc.) et de conserver la possibilité de lecture nettoyante le reste du temps. La grille se souvient de l’état de bascule pour la session.
<GridColumn ColumnName="..." /> Les déclarations sans ChildContent auto-dispatch font passer l’éditeur en ligne droit pendant que la grille est en mode édition. Le framework lit les métadonnées de la colonne et choisit la sous-classe éditeur :
String, Memo, EmailAddress, Phone, Url — TextEdit (avec validation adaptée au format).Integer, Decimal, Double, BigInt — NumberEdit (analyse adaptée à la localisation, min/max à partir des métadonnées).Boolean — BoolEdit (rendu comme un interrupteur, alignement centré).Picklist — ChoiceEdit (menu déroulant piloté par l’ensemble d’options, labels localisés).MultiSelectPicklist — MultiSelectChoiceEdit.Lookup, Customer, Owner — LookupEdit (typeahead, respecte les listes de permis dans la table cible).DateTime, DateOnly — DateTimeEdit.Money— MoneyEdit (préfixe symbole monétaire de ).transactioncurrencyidFile, Image — / FileEdit ImageEdit (téléchargement glisser-déposé).Fournit ChildContent sur a GridColumn pour prendre en charge à la fois l’affichage en mode lecture ET l’éditeur en mode édition pour cette colonne. Une fois ChildContent configuré, le framework cesse de déployer automatiquement l’éditeur piloté par les métadonnées — affichez l’éditeur que vous souhaitez dans le modèle et bloquez sa visibilité hors du drapeau de Editable la grille. Les éditeurs standards (TextEdit, NumberEdit, ColumnEdit) s’installent automatiquement dans le modèle pour le suivi sale et la validation via le contexte de ligne en cascade.
{/* Cellule d’affichage personnalisée PLUS éditeur personnalisé. React sépare les deux
en moitié en cellRenderer (lecture seule) et editRenderer (mode édit) —
utiliser <GridTemplateColumn> pour que les deux moteurs de rendu puissent coexister pour le</GridTemplateColumn>
Même colonne logique. Les <TextEdit> auto-registres en drop-in pour</TextEdit>
suivi sale et validation via le contexte de la ligne en cascade. */}
<GridTemplateColumn
displayName="Email"
dependsOn={['emailaddress1']}
sortBy="emailaddress1"
cellRenderer={({ record }) => {
const email = (record.properties?.emailaddress1 as { value?: string })?.value;
return <span>{email}</span>;
}}
editRenderer={() => (
<TextEdit columnName="emailaddress1" displayLabelWhenAvailable={false} />
)}
/><!-- Cellule d’affichage personnalisée PLUS éditeur personnalisé. Une fois que ChildContent est
Fourni, le framework ne dépêche plus automatiquement un Inline
éditeur pour cette chronique, alors rendez-en une vous-même si vous le souhaitez
la cellule modifiable. L’éditeur intégré s’auto-enregistre pour
suivi sale et validation via le contexte de la ligne en cascade. -->
<GridColumn ColumnName="emailaddress1">
<ChildContent>
@{
var email = context.PrimaryRecord
.GetValueOrDefault<StringValue>("emailaddress1")?.Value;
}
@if (this.Editable)
{
<TextEdit ColumnName="emailaddress1"
DisplayLabelWhenAvailable="false"
DisplayTooltipWhenAvailable="false" />
}
else
{
<span>@email</span>
}
</ChildContent>
</GridColumn>Chaque ligne conserve son propre EditContextValidator. Les éditeurs standards enregistrent avec elle sur montage et affichent leurs erreurs en ligne (sous-ligne rouge + infobulle sur la cellule). Le bouton d’enregistrement de la grille est désactivé tandis que toute ligne présente une erreur de validation non résolue. Les règles de validation proviennent des métadonnées de la colonne :
RequiredLevel — la colonne est requise, et une valeur vide bloque sauvegarde.Format sur les colonnes de chaîne — les formats email, téléphone, URL imposent une forme au-dessus du type.MinValue / MaxValue sur les colonnes numériques — les valeurs hors plage sont signalées avant la sauvegarde.EditContextValidator pour les invariants inter-champs (par exemple « le contact doit avoir soit un e-mail soit un téléphone »).Validation sur des widgets personnalisés
Les entrées personnalisées rendues à l’intérieur
ChildContent/EditChildContentqui ne s’étendentBaseEditpas (par exemple un widget brutFluentTextFieldou tiers) ne s’enregistrent pas automatiquement avec le validateur de la ligne. Si vous avez besoin qu’ils soient validés, soit vous pouvez supprimer un éditeur standard à la place du widget brut, soit exécuter la validation manuellement dans le rappel duValueChangedwidget avant d’appelereditCtx.SetValue.
Les modifications s’accumulent en tant que mises à jour en attente sur la ligne — les cellules modifiées reçoivent un petit indicateur « sale », les lignes supprimées sont barrées, et les lignes en attente de création apparaissent comme nouvelles. La barre d’outils de la grille affiche les boutons Enregistrer et Annuler chaque fois qu’il y a des modifications en attente : Sauvegarder envoie le batch en un seul passage transactionnel ; Annuler défausse le tampon et ramène chaque ligne à son état serveur. Associez-le avec NewRecordGridButton et DeleteRecordGridButton dans le Buttons fragment pour ajouter la création et la suppression de lignes à la même transaction d’édition.
<MainGrid tableName="contact" allowEdit>
<GridColumns>
<GridColumn columnName="firstname" />
<GridColumn columnName="lastname" />
<GridColumn columnName="emailaddress1" />
</GridColumns>
<GridButtons>
<NewRecordGridButton>
<NewContactForm />
</NewRecordGridButton>
<DeleteRecordGridButton />
</GridButtons>
</MainGrid><MainGrid TableName="contact" AllowEdit="true">
<GridColumns>
<GridColumn ColumnName="firstname" />
<GridColumn ColumnName="lastname" />
<GridColumn ColumnName="emailaddress1" />
</GridColumns>
<Buttons>
<NewRecordGridButton TForm="NewContactForm" />
<DeleteRecordGridButton />
</Buttons>
</MainGrid>Associez le mode ChildContent lecture de a GridTemplateColumnà un EditChildContent pour rendre une colonne synthétique / composite modifiable. Le modèle d’édition reçoit un GridTemplateColumnEditContext — même contexte de ligne que le modèle de lecture (editCtx.Row.PrimaryRecord, l’optionnel editCtx.Row.ParentRecord dans une sous-grille, editCtx.DependsOnMetadata) plus un assistant impératifeditCtx.SetValue(columnName, value).
EditChildContent ne se déclenche que lorsque la grille est modifiable (AllowEdit="true" ET que le bouton Modification est activé) et que la ligne n’est pas en attente-suppression. Les colonnes modèles qui omettent EditChildContent restent en lecture seule même en mode édition — utiles pour les colonnes d’actions qui devraient toujours afficher les mêmes boutons.
Le cas le plus courant : la cellule est composée de plusieurs colonnes liées, et vous voulez laisser l’utilisateur modifier chacune d’elles. Ajoutez les éditeurs correspondants <TextEdit> / <ColumnEdit> <NumberEdit> / à l’intérieur EditChildContent — le framework cascade l’enregistrement principal et la correspondance EditContextValidator de la ligne dans cette catégorie, afin que les éditeurs s’auto-enregistrent pour le dirty tracking et la validation. Aucun câblage supplémentaire n’est nécessaire.
<GridTemplateColumn
displayName="Nom complet"
dependsOn={['firstname', 'lastname']}
sortBy="lastname"
cellRenderer={({ record }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
return <strong>{first} {last}</strong>;
}}
// Éditeurs à accès direct — TextEdit se connecte automatiquement au suivi sale
// et la validation via le contexte de la ligne en cascade.
editRenderer={() => (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<TextEdit columnName="firstname" displayLabelWhenAvailable={false} />
<TextEdit columnName="lastname" displayLabelWhenAvailable={false} />
</div>
)}
/><GridTemplateColumn Title="Nom complet"
DependsOn="@(new[] { "firstname", "lastname" })"
SortBy="lastname">
<ChildContent Context="ctx">
<strong>
@(ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("firstname")?.Value)
@(ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("lastname")?.Value)
</strong>
</ChildContent>
<!-- Éditeurs à accès direct — TextEdit se connecte automatiquement au suivi sale
et la validation via le contexte de la ligne en cascade. -->
<EditChildContent Context="editCtx">
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="6"
VerticalAlignment="VerticalAlignment.Center">
<TextEdit ColumnName="firstname"
DisplayLabelWhenAvailable="false"
DisplayTooltipWhenAvailable="false" />
<TextEdit ColumnName="lastname"
DisplayLabelWhenAvailable="false"
DisplayTooltipWhenAvailable="false" />
</FluentStack>
</EditChildContent>
</GridTemplateColumn>Lorsque la surface de l’éditeur ne correspond pas en 1:1 à une colonne Dataverse — une seule boîte de texte « Premier dernier » qui se divise en deux colonnes, un curseur lié à plusieurs champs de pourcentage, un sélecteur personnalisé par plage de dates écrivant début + fin — affichez votre propre entrée et appelez editCtx.SetValue(columnName, value) chaque colonne affectée. Le framework redirige l’écriture vers la bonne AliasedTableRecord et lance ValueChanged , donc le dirty-tracking récupère le changement.
<GridTemplateColumn
displayName="Nom complet"
dependsOn={['firstname', 'lastname']}
sortBy="lastname"
cellRenderer={({ record }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
return <strong>{first} {last}</strong>;
}}
editRenderer={({ record, setValue }) => {
const first = (record.properties?.firstname as { value?: string })?.value ?? '';
const last = (record.properties?.lastname as { value?: string })?.value ?? '';
const combined = [first, last].filter(Boolean).join(' ');
// Widget personnalisé — découpé sur l’espace ; Les avis des consommateurs sur
// validation puisque <Input> ne s’enregistre pas tout seul.
return (
<Input
value={combined}
onChange={(_, data) => {
const next = data.value ?? '';
const idx = next.indexOf(' ');
const head = idx === -1 ? next : next.slice(0, idx);
const tail = idx === -1 ? '' : next.slice(idx + 1);
setValue('firstname', { $type: 'StringValue', value: head });
setValue('lastname', { $type: 'StringValue', value: tail || null });
}}
/>
);
}}
/><GridTemplateColumn Title="Nom complet"
DependsOn="@(new[] { "firstname", "lastname" })"
SortBy="lastname">
<ChildContent Context="ctx">
@{
var first = ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("firstname")?.Value;
var last = ctx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("lastname")?.Value;
}
<strong>@first @last</strong>
</ChildContent>
<EditChildContent Context="editCtx">
@{
var combined =
(editCtx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("firstname")?.Value ?? "")
+ " "
+ (editCtx.Row.PrimaryRecord.GetValueOrDefault<StringValue>("lastname")?.Value ?? "");
}
<!-- Widget personnalisé — découpé sur l’espace ; Les avis des consommateurs sur
validation puisque FluentTextField ne s’enregistre pas automatiquement. -->
<FluentTextField Value="@combined.Trim()" ValueChanged="@(next =>
{
var parts = next.Split(' ', 2);
editCtx.SetValue("firstname", new StringValue { Value = parts[0] });
editCtx.SetValue("lastname", new StringValue { Value = parts.Length > 1 ? parts[1] : null });
})" />
</EditChildContent>
</GridTemplateColumn>Validation sur le chemin impératif
Les éditeurs standards s’enregistraient
EditChildContentautomatiquement avec lesEditContextValidatorlignes et participaient à la porte de validation avant la sauvegarde du framework. LeeditCtx.SetValuechemin contourne ce pipeline — les valeurs qui ne proviennent pas d’un éditeur enregistré ne sont pas validées. Si votre widget personnalisé doit imposer des contraintes, exécutez-les dans leValueChangedrappel avant d’appelerSetValue, ou revenez au modèle d’éditeur en drop-in.