Compartilhar via


Tutorial: Criando um componente de conjunto de dados do aplicativo Canvas

Neste tutorial, você criará um componente de código para dataset em um aplicativo Canvas, o implantará, adicioná-lo à interface de usuário, e testará o componente usando Visual Studio Code. O componente de código exibe uma grade de conjunto de dados paginável e rolável que fornece colunas classificáveis e filtradas. Ele também permite o realce de linhas específicas configurando uma coluna de indicador. Essa é uma solicitação comum dos criadores de aplicativos e pode ser complexa de implementar usando componentes de aplicativo de tela nativa. Os componentes de código podem ser escritos para funcionar em aplicativos de canvas e em aplicativos de modelo. No entanto, esse componente foi desenvolvido especificamente para ser usado em aplicativos de canvas.

Além desses requisitos, você também garantirá que o componente de código siga as diretrizes de prática recomendada:

  1. Uso da interface do usuário do Microsoft Fluent
  2. Localização dos rótulos dos componentes de código tanto no design quanto no tempo de execução
  3. Garantir que o componente de código seja renderizado na largura e altura fornecidas pela tela do aplicativo de canvas pai
  4. Consideração para o criador de aplicativos personalizar a interface do usuário usando propriedades de entrada e elementos de aplicativo externos na medida do possível

Demonstração da Grade de Canvas

Observação

Antes de começar, verifique se você instalou todos os componentes de pré-requisito.

Code

Você pode baixar o exemplo completo do PowerApps-Samples/component-framework/CanvasGridControl/.

Criar um novo projeto pcfproj

  1. Crie uma nova pasta a ser usada para o componente de código. Por exemplo, C:\repos\CanvasGrid.

  2. Abra Visual Studio Code e File>Open Folder e selecione a pasta CanvasGrid. Se você adicionou as extensões Windows Explorer durante a instalação do Visual Studio Code, poderá usar a opção de menu de contexto Open com Code dentro da pasta. Você também pode carregar qualquer pasta em Visual Studio Code usando code . no prompt de comando quando o diretório atual estiver definido para esse local.

  3. Dentro de um novo terminal do PowerShell Visual Studio Code (Terminal>New Terminal), use o comando pac pcf init para criar um novo projeto de componente de código:

    pac pcf init --namespace SampleNamespace --name CanvasGrid --template dataset
    

    ou usando o formulário curto:

    pac pcf init -ns SampleNamespace -n CanvasGrid -t dataset
    
  4. Isso adiciona arquivos novos pcfproj e relacionados à pasta atual, incluindo um packages.json que define os módulos necessários. Para instalar os módulos necessários, use a instalação do npm:

    npm install
    

    Observação

    Se você receber a mensagem, The term 'npm' is not recognized as the name of a cmdlet, function, script file, or operable program.verifique se instalou todos os pré-requisitos, especificamente node.js (a versão lts é recomendada).

    Grade de conjunto de dados do quadro

O modelo inclui um index.ts arquivo junto com vários arquivos de configuração. Esse é o ponto inicial do componente de código e contém os métodos de ciclo de vida descritos na implementação do componente.

Instalar a interface do usuário do Microsoft Fluent

Você usará o Microsoft Fluent UI e o React para criar a interface do usuário, portanto, deve instalá-los como dependências. Use o seguinte no terminal:

npm install react react-dom @fluentui/react

Isso acrescenta os módulos ao packages.json e os instala no diretório node_modules. Você não fará o commit de node_modules no controle de código-fonte, pois todos os módulos necessários podem ser restaurados usando npm install.

Uma das vantagens da interface do usuário do Microsoft Fluent é que ela fornece uma interface do usuário consistente e altamente acessível .

Configurando eslint

O modelo usado pelo pac pcf init instala o eslint módulo em seu projeto e o configura adicionando um .eslintrc.json arquivo. Eslint agora requer a configuração para os estilos de codificação TypeScript e React. Mais informações: Linting – Práticas recomendadas e diretrizes para componentes de código.

Definir as propriedades do conjunto de dados

O CanvasGrid\ControlManifest.Input.xml arquivo define os metadados que descrevem o comportamento do componente de código. O atributo de controle já conterá o namespace e o nome do componente.

Dica

Você pode achar o XML mais fácil de ler formatando-o para que os atributos apareçam em linhas separadas. Localize e instale uma ferramenta de formatação XML de sua escolha no Visual Studio Code Marketplace: Search para extensões de formatação xml.

Os exemplos abaixo foram formatados com atributos em linhas separadas para facilitar a leitura.

Você deve definir os registros aos quais o componente de código pode ser associado, adicionando o seguinte dentro do control elemento, substituindo o elemento existente data-set :

<data-set name="sampleDataSet"
  display-name-key="Dataset_Display_Key">
</data-set>

O conjunto de dados registros será associado a uma fonte de dados quando o componente de código for adicionado a um aplicativo de tela. O conjunto de propriedades indica que o usuário deve configurar uma das colunas desse conjunto de dados a serem usadas como o indicador de realce de linha.

Dica

Você pode especificar vários elementos de conjunto de dados. Isso pode ser útil se você quiser pesquisar um dataset, mas mostrar uma lista de registros usando um segundo dataset.

Definindo as propriedades de entrada e saída

Além do conjunto de dados, você pode fornecer as seguintes propriedades de entrada :

  • HighlightValue - Permite que o criador de aplicativos forneça um valor a ser comparado com a coluna definida como HighlightIndicatorproperty-set. Quando os valores são iguais, a linha deve ser realçada.
  • HighlightColor - Permite que o criador de aplicativos selecione uma cor a ser usada para realçar linhas.

Dica

Ao criar componentes de código para uso em aplicativos canvas, é recomendável fornecer propriedades de entrada para a estilização de elementos comuns dos componentes de código.

Além das propriedades de entrada, uma propriedade de saída nomeada FilteredRecordCount será atualizada (e disparará o OnChange evento) quando a contagem de linhas for alterada devido a uma ação de filtro aplicada dentro do componente de código. Isso é útil quando você deseja exibir uma mensagem No Rows Found dentro do aplicativo pai.

Observação

No futuro, os componentes de código darão suporte a eventos personalizados para que você possa definir um evento específico em vez de usar o evento genérico OnChange .

Para definir essas três propriedades, adicione o seguinte ao CanvasGrid\ControlManifest.Input.xml arquivo, abaixo do data-set elemento:

<property name="FilteredRecordCount"
  display-name-key="FilteredRecordCount_Disp"
  description-key="FilteredRecordCount_Desc"
  of-type="Whole.None"
  usage="output" />
<property name="HighlightValue"
  display-name-key="HighlightValue_Disp"
  description-key="HighlightValue_Desc"
  of-type="SingleLine.Text"
  usage="input"
  required="true"/>
<property name="HighlightColor"
  display-name-key="HighlightColor_Disp"
  description-key="HighlightColor_Desc"
  of-type="SingleLine.Text"
  usage="input"
  required="true"/>

Salve este arquivo e, em seguida, na linha de comando, use:

npm run build

Observação

Se você receber um erro como este durante a execução npm run build:

[2:48:57 PM] [build]  Running ESLint...
[2:48:57 PM] [build]  Failed:
[pcf-1065] [Error] ESLint validation error:
C:\repos\CanvasGrid\CanvasGrid\index.ts
  2:47  error  'PropertyHelper' is not defined  no-undef

Abra index.ts arquivo e adicione o seguinte: // eslint-disable-next-line no-undef, diretamente acima da linha:
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;

Execute a npm run build novamente.

Depois que o componente for criado, você verá que:

  • Um arquivo CanvasGrid\generated\ManifestTypes.d.ts gerado automaticamente é adicionado ao seu projeto. Isso é gerado como parte do processo de build a partir do ControlManifest.Input.xml e fornece os tipos para interagir com as propriedades de entrada/saída.

  • A saída de build é adicionada à out pasta. É bundle.js o JavaScript transpilado que é executado dentro do navegador, e ControlManifest.xml é uma versão refatorada do arquivo ControlManifest.Input.xml que é usada durante a implementação.

    Observação

    Não modifique diretamente o conteúdo das pastas generated e out. Eles serão substituídos como parte do processo de build.

Adicionar o componente Grid Fluent UI React

Quando o componente de código usa React, deve haver um único componente raiz renderizado dentro do método updateView. Dentro da CanvasGrid pasta, adicione um novo arquivo TypeScript chamado Grid.tsxe adicione o seguinte conteúdo:

import {
    DetailsList,
    ConstrainMode,
    DetailsListLayoutMode,
    IColumn,
    IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import { 
   ScrollablePane, 
   ScrollbarVisibility 
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';

type DataSet = ComponentFramework.PropertyHelper.DataSetApi.EntityRecord & IObjectWithKey;

export interface GridProps {
    width?: number;
    height?: number;
    columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
    records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
    sortedRecordIds: string[];
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    totalResultCount: number;
    currentPage: number;
    sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
    filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
    resources: ComponentFramework.Resources;
    itemsLoading: boolean;
    highlightValue: string | null;
    highlightColor: string | null;
}

const onRenderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props, defaultRender) => {
    if (props && defaultRender) {
        return (
            <Sticky stickyPosition={StickyPositionType.Header} isScrollSynced>
                {defaultRender({
                    ...props,
                })}
            </Sticky>
        );
    }
    return null;
};

const onRenderItemColumn = (
    item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord,
    index?: number,
    column?: IColumn,
) => {
    if (column && column.fieldName && item) {
        return <>{item?.getFormattedValue(column.fieldName)}</>;
    }
    return <></>;
};

export const Grid = React.memo((props: GridProps) => {
    const {
        records,
        sortedRecordIds,
        columns,
        width,
        height,
        hasNextPage,
        hasPreviousPage,
        sorting,
        filtering,
        currentPage,
        itemsLoading,
    } = props;

    const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);

    const items: (DataSet | undefined)[] = React.useMemo(() => {
        setIsLoading(false);

        const sortedRecords: (DataSet | undefined)[] = sortedRecordIds.map((id) => {
            const record = records[id];
            return record;
        });

        return sortedRecords;
    }, [records, sortedRecordIds, hasNextPage, setIsLoading]);

    const gridColumns = React.useMemo(() => {
        return columns
            .filter((col) => !col.isHidden && col.order >= 0)
            .sort((a, b) => a.order - b.order)
            .map((col) => {
                const sortOn = sorting && sorting.find((s) => s.name === col.name);
                const filtered =
                    filtering && 
                    filtering.conditions && 
                    filtering.conditions.find((f) => f.attributeName == col.name);
                return {
                    key: col.name,
                    name: col.displayName,
                    fieldName: col.name,
                    isSorted: sortOn != null,
                    isSortedDescending: sortOn?.sortDirection === 1,
                    isResizable: true,
                    isFiltered: filtered != null,
                    data: col,
                } as IColumn;
            });
    }, [columns, sorting]);

    const rootContainerStyle: React.CSSProperties = React.useMemo(() => {
        return {
            height: height,
            width: width,
        };
    }, [width, height]);

    return (
        <Stack verticalFill grow style={rootContainerStyle}>
            <Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
                <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
                    <DetailsList
                        columns={gridColumns}
                        onRenderItemColumn={onRenderItemColumn}
                        onRenderDetailsHeader={onRenderDetailsHeader}
                        items={items}
                        setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
                        initialFocusedIndex={0}
                        checkButtonAriaLabel="select row"
                        layoutMode={DetailsListLayoutMode.fixedColumns}
                        constrainMode={ConstrainMode.unconstrained}
                    ></DetailsList>
                </ScrollablePane>
                {(itemsLoading || isComponentLoading) && <Overlay />}
            </Stack.Item>
        </Stack>
    );
});

Grid.displayName = 'Grid';

Observação

O arquivo tem a extensão tsx que é um arquivo TypeScript que dá suporte à sintaxe de estilo XML usada pelo React. Ele é compilado em JavaScript padrão pelo processo de build.

Notas de design de grade

Esta seção inclui comentários sobre o design do Grid.tsx componente.

É um componente funcional

Esse é um componente funcional do React, mas, igualmente, pode ser um componente de classe. Isso se baseia no seu estilo de codificação preferido. Componentes de classe e componentes funcionais também podem ser misturados no mesmo projeto. Os componentes de função e classe usam a tsx sintaxe de estilo XML usada pelo React. Mais informações: Componentes de função e classe

Minimizar o tamanho do bundle.js

Ao importar os componentes Fluent UI ChoiceGroup usando importações baseadas em caminho, em vez de:

import { 
    DetailsList, 
    ConstrainMode, 
    DetailsListLayoutMode, 
    IColumn, 
    IDetailsHeaderProps, 
    Stack 
} from "@fluentui/react";

Este código usa:

import {
    DetailsList,
    ConstrainMode,
    DetailsListLayoutMode,
    IColumn,
    IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Stack } from '@fluentui/react/lib/Stack';

Dessa forma, o tamanho do pacote será menor, resultando em requisitos de capacidade mais baixos e melhor desempenho de runtime.

Uma alternativa seria usar o tree-shaking.

Atribuição de desestruturação

Esse código:

export const Grid = React.memo((props: GridProps) => {
    const {
        records,
        sortedRecordIds,
        columns,
        width,
        height,
        hasNextPage,
        hasPreviousPage,
        sorting,
        filtering,
        currentPage,
        itemsLoading,
    } = props;

Usa a atribuição de estruturação. Dessa forma, você extrai os atributos necessários para renderizar dos adereços, em vez de prefixá-los com props. cada vez que eles são usados.

O código também usa React.memo para encapsular o componente funcional para que ele não seja renderizado, a menos que qualquer um dos adereços de entrada tenha sido alterado.

Uso de React.useMemo

React.useMemo é utilizado em vários lugares para garantir que a matriz de itens criada seja alterada apenas quando as propriedades de entrada, options ou configuration, forem modificadas. Essa é uma prática recomendada de componentes de função que reduz renderizações desnecessárias dos componentes filho.

Outros itens a serem observados:

  • O DetailsList em um Stack é envolvido porque, posteriormente, você adicionará um elemento de rodapé com os controles de paginação.
  • O componente Fluent UI Sticky é usado para encapsular as colunas de cabeçalho (usando onRenderDetailsHeader) para que elas permaneçam visíveis quando a grade é rolada.
  • setKey é passado para o DetailsList junto com initialFocusedIndex de modo que, quando a página atual for alterada, a posição de rolagem e a seleção serão redefinidas.
  • A função onRenderItemColumn é usada para renderizar o conteúdo da célula. Ele aceita o item de linha e usa getFormattedValue para retornar o valor de exibição da coluna. O método getValue retorna um valor que você pode usar para fornecer uma renderização alternativa. A vantagem de getFormattedValue é que ele contém uma cadeia de caracteres formatada para colunas de tipos não textuais, como datas e consultas.
  • O bloco gridColumns está mapeando a forma do objeto das colunas fornecidas pelo contexto do conjunto de dados para a forma esperada pela propriedade colunas de DetailsList. Como isso está encapsulado no React.useMemo hook, a saída só será alterada quando as propriedades columns ou sorting forem alteradas. Você pode exibir os ícones de classificação e filtro nas colunas em que os detalhes de classificação e filtragem fornecidos pelo contexto do componente de código correspondem à coluna que está sendo mapeada. As colunas são classificadas usando a column.order propriedade para garantir que elas estejam na ordem correta na grade, conforme definido pelo criador de aplicativos.
  • Você está mantendo um estado interno para isComponentLoading no nosso componente React. Isso ocorre porque, quando o usuário seleciona ações de classificação e filtragem, você pode esmaecer a grade como uma pista visual até que o elemento sortedRecordIds seja atualizado e o estado seja redefinido. Há uma propriedade de entrada adicional chamada itemsLoading que é mapeada para a propriedade dataset.loading fornecida pelo contexto de dados. Ambos os flags são usados para controlar a indicação de carregamento visual implementada usando o componente Fluent UI Overlay.

Atualizar index.ts

A próxima etapa é fazer alterações no index.ts arquivo para corresponder às propriedades definidas em Grid.tsx.

Adicionar instruções de importação e inicializar ícones

Para o cabeçalho de index.ts, substitua as importações existentes pelas seguintes:

import {IInputs, IOutputs} from './generated/ManifestTypes';
import DataSetInterfaces = ComponentFramework.PropertyHelper.DataSetApi;
type DataSet = ComponentFramework.PropertyTypes.DataSet;

Observação

A importação de initializeIcons é necessária porque esse código usa o conjunto de ícones do Fluent UI. Você chama initializeIcons para carregar os ícones dentro do cinto de teste. Dentro de aplicativos de 'canvas', já estão inicializados.

Adicionar campos à classe CanvasGrid

Adicione os seguintes campos à CanvasGrid classe:

export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {

    /**
     * Empty constructor.
     */
    constructor() {

    }

Atualizar o método de inicialização

Adicione o seguinte a init:

public init(
    context: ComponentFramework.Context<IInputs>, 
    notifyOutputChanged: () => void, 
    state: ComponentFramework.Dictionary, 
    container: HTMLDivElement): void {
    // Add control initialization code
}

A init função é chamada quando o componente de código é inicializado pela primeira vez em uma tela do aplicativo. Você armazena uma referência para o seguinte:

  • notifyOutputChanged: Esta é a função de retorno de chamada que você utiliza para notificar o aplicativo Canvas de que uma das propriedades foi alterada.
  • container:Este é o elemento DOM ao qual você adiciona a interface do usuário do componente de código.
  • resources:Isso é usado para recuperar cadeias de caracteres localizadas no idioma do usuário atual.

O context.mode.trackContainerResize(true)) é utilizado para que updateView seja chamado quando o elemento de código alterar seu tamanho.

Observação

Atualmente, não há como determinar se o componente de código está em execução dentro do cinto de teste. Você precisa detectar se o control-dimensionsdiv elemento está presente como um indicador.

Atualizar o método updateView

Adicione o seguinte a updateView:

public updateView(context: ComponentFramework.Context<IInputs>): void {
    // Add code to update control view
}

Você pode ver que:

  • Você chama React.createElement, passando a referência para o container DOM recebido dentro da função init.
  • O Grid componente é definido dentro Grid.tsx e é importado na parte superior do arquivo.
  • O allocatedWidth e allocatedHeight serão fornecidos pelo contexto pai sempre que houver uma alteração (por exemplo, o aplicativo redimensiona o componente de código ou você entra no modo de tela inteira), já que você fez uma chamada para trackContainerResize(true) dentro da função init.
  • Você pode detectar quando há novas linhas a serem exibidas quando a matriz updatedProperties contiver a dataset cadeia de caracteres.
  • No ambiente de teste, a updatedProperties matriz não é preenchida, portanto, você pode usar o isTestHarness sinalizador que você definiu na init função para interromper a lógica que define o sortedRecordId e records. Você mantém uma referência aos valores atuais até que eles sejam alterados, de modo que você não os altere quando passado para o componente filho, a menos que seja necessária uma nova renderização dos dados.
  • Como o componente de código mantém o estado da página exibida, o número da página é redefinido quando o contexto pai redefine os registros para a primeira página. Você sabe quando está de volta à primeira página quando hasPreviousPage é falso.

Atualizar o método destroy

Por fim, você precisa arrumar quando o componente de código é destruído:

public destroy(): void {
    // Add code to cleanup control if necessary
}

Iniciar o arreio de teste

Verifique se todos os arquivos foram salvos e, no terminal, use:

npm start watch

Você precisa definir a largura e a altura para ver a grade de componentes de código preenchida usando os três registros de exemplo. Em seguida, você pode exportar um conjunto de registros para um arquivo CSV do Dataverse e carregá-lo no harness de teste usando o painel Entradas>de Registros de Dados.

Arreio de teste

Aqui estão alguns dados de exemplo separados por vírgula que você pode salvar em um arquivo .csv e usar:

address1_city,address1_country,address1_stateorprovince,address1_line1,address1_postalcode,telephone1,emailaddress1,firstname,fullname,jobtitle,lastname
Seattle,U.S.,WA,7842 Ygnacio Valley Road,12150,555-0112,someone_m@example.com,Thomas,Thomas Andersen (sample),Purchasing Manager,Andersen (sample)
Renton,U.S.,WA,7165 Brock Lane,61795,555-0109,someone_j@example.com,Jim,Jim Glynn (sample),Owner,Glynn (sample)
Snohomish,U.S.,WA,7230 Berrellesa Street,78800,555-0106,someone_g@example.com,Robert,Robert Lyon (sample),Owner,Lyon (sample)
Seattle,U.S.,WA,931 Corte De Luna,79465,555-0111,someone_l@example.com,Susan,Susan Burk (sample),Owner,Burk (sample)
Seattle,U.S.,WA,7765 Sunsine Drive,11910,555-0110,someone_k@example.com,Patrick,Patrick Sands (sample),Owner,Sands (sample)
Seattle,U.S.,WA,4948 West Th St,73683,555-0108,someone_i@example.com,Rene,Rene Valdes (sample),Purchasing Assistant,Valdes (sample)
Redmond,U.S.,WA,7723 Firestone Drive,32147,555-0107,someone_h@example.com,Paul,Paul Cannon (sample),Purchasing Assistant,Cannon (sample)
Issaquah,U.S.,WA,989 Caravelle Ct,33597,555-0105,someone_f@example.com,Scott,Scott Konersmann (sample),Purchasing Manager,Konersmann (sample)
Issaquah,U.S.,WA,7691 Benedict Ct.,57065,555-0104,someone_e@example.com,Sidney,Sidney Higa (sample),Owner,Higa (sample)
Monroe,U.S.,WA,3747 Likins Avenue,37925,555-0103,someone_d@example.com,Maria,Maria Campbell (sample),Purchasing Manager,Campbell (sample)
Duvall,U.S.,WA,5086 Nottingham Place,16982,555-0102,someone_c@example.com,Nancy,Nancy Anderson (sample),Purchasing Assistant,Anderson (sample)
Issaquah,U.S.,WA,5979 El Pueblo,23382,555-0101,someone_b@example.com,Susanna,Susanna Stubberod (sample),Purchasing Manager,Stubberod (sample)
Redmond,U.S.,WA,249 Alexander Pl.,86372,555-0100,someone_a@example.com,Yvonne,Yvonne McKay (sample),Purchasing Manager,McKay (sample)

Observação

Apenas uma coluna é mostrada no harnês de teste, independentemente das colunas fornecidas no arquivo CSV carregado. Isso ocorre porque a ferramenta de teste apenas mostra property-set quando há uma definida. Se não for property-set definido, todas as colunas no arquivo CSV carregado serão preenchidas.

Adicionar seleção de linha

Embora a interface DetailsList do usuário fluente permita a seleção de registros por padrão, os registros selecionados não estão vinculados à saída do componente de código. Você precisa das propriedades Selected e SelectedItems para refletir os registros escolhidos em um app de tela, para que os componentes relacionados possam ser atualizados. Neste exemplo, você permite a seleção de apenas um único item de cada vez, portanto SelectedItems , só conterá um único registro.

Atualizar importações grid.tsx

Adicione o seguinte às importações dentro Grid.tsx:

import {
    DetailsList,
    ConstrainMode,
    DetailsListLayoutMode,
    IColumn,
    IDetailsHeaderProps,
} from '@fluentui/react/lib/DetailsList';
import { Overlay } from '@fluentui/react/lib/Overlay';
import { 
   ScrollablePane, 
   ScrollbarVisibility 
} from '@fluentui/react/lib/ScrollablePane';
import { Stack } from '@fluentui/react/lib/Stack';
import { Sticky } from '@fluentui/react/lib/Sticky';
import { StickyPositionType } from '@fluentui/react/lib/Sticky';
import { IObjectWithKey } from '@fluentui/react/lib/Selection';
import { IRenderFunction } from '@fluentui/react/lib/Utilities';
import * as React from 'react';

Adicionar setSelectedRecords a GridProps

Para a GridProps interface, dentro de Grid.tsx, adicione o seguinte:

export interface GridProps {
    width?: number;
    height?: number;
    columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
    records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
    sortedRecordIds: string[];
    hasNextPage: boolean;
    hasPreviousPage: boolean;
    totalResultCount: number;
    currentPage: number;
    sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
    filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
    resources: ComponentFramework.Resources;
    itemsLoading: boolean;
    highlightValue: string | null;
    highlightColor: string | null;
}

Adicionar a propriedade setSelectedRecords ao Grid

Dentro do componente de função Grid.tsx, atualize a desestruturação do props para adicionar a nova propriedade setSelectedRecords.

export const Grid = React.memo((props: GridProps) => {
    const {
        records,
        sortedRecordIds,
        columns,
        width,
        height,
        hasNextPage,
        hasPreviousPage,
        sorting,
        filtering,
        currentPage,
        itemsLoading,
    } = props;

Diretamente abaixo disso, adicione:

const forceUpdate = useForceUpdate();
const onSelectionChanged = React.useCallback(() => {
  const items = selection.getItems() as DataSet[];
  const selected = selection.getSelectedIndices().map((index: number) => {
    const item: DataSet | undefined = items[index];
    return item && items[index].getRecordId();
  });

  setSelectedRecords(selected);
  forceUpdate();
}, [forceUpdate]);

const selection: Selection = useConst(() => {
  return new Selection({
    selectionMode: SelectionMode.single,
    onSelectionChanged: onSelectionChanged,
  });
});

Os ganchos React.useCallback e useConst garantem que esses valores não sejam modificados entre renderizações e causem renderização de componente filho desnecessária.

O gancho useForceUpdate garante que, quando a seleção for atualizada, o componente seja renderizado novamente para refletir a contagem de seleção atualizada.

Adicionar seleção a DetailsList

O selection objeto criado para manter o estado da seleção é então passado para o DetailsList componente:

<DetailsList
   columns={gridColumns}
   onRenderItemColumn={onRenderItemColumn}
   onRenderDetailsHeader={onRenderDetailsHeader}
   items={items}
   setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
   initialFocusedIndex={0}
   checkButtonAriaLabel="select row"
   layoutMode={DetailsListLayoutMode.fixedColumns}
   constrainMode={ConstrainMode.unconstrained}
></DetailsList>

Definir o callback de setSelectedRecords

Você precisa definir o novo setSelectedRecords callback dentro de index.ts e passá-lo para o componente Grid. Próximo à parte superior da CanvasGrid classe, adicione o seguinte:

export class CanvasGrid
  implements ComponentFramework.StandardControl<IInputs, IOutputs>
{
  notifyOutputChanged: () => void;
  container: HTMLDivElement;
  context: ComponentFramework.Context<IInputs>;
  sortedRecordsIds: string[] = [];
  resources: ComponentFramework.Resources;
  isTestHarness: boolean;
  records: {
    [id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
  };
  currentPage = 1;
  filteredRecordCount?: number;

Observação

O método é definido como uma função de seta para associá-lo à instância atual this do componente de código.

A chamada para setSelectedRecordIds informa ao aplicativo de tela que a seleção foi alterada para que os outros componentes que referenciam SelectedItems e Selected sejam atualizados.

Adicionar novo callback às propriedades de entrada

Por fim, adicione o novo callback às propriedades de entrada do componente Grid no método updateView.

ReactDOM.render(
 React.createElement(Grid, {
   width: allocatedWidth,
   height: allocatedHeight,
   columns: dataset.columns,
   records: this.records,
   sortedRecordIds: this.sortedRecordsIds,
   hasNextPage: paging.hasNextPage,
   hasPreviousPage: paging.hasPreviousPage,
   currentPage: this.currentPage,
   totalResultCount: paging.totalResultCount,
   sorting: dataset.sorting,
   filtering: dataset.filtering && dataset.filtering.getFilter(),
   resources: this.resources,
   itemsLoading: dataset.loading,
   highlightValue: this.context.parameters.HighlightValue.raw,
   highlightColor: this.context.parameters.HighlightColor.raw,
 }),
this.container
);

Invocando o OnSelect evento

Há um padrão em aplicativos Canvas em que, se uma galeria ou grade tiver uma seleção de item invocada (por exemplo, selecionando um ícone de seta), ele aciona o evento OnSelect. Você pode implementar esse padrão usando o método openDatasetItem do conjunto de dados.

Adicionar onNavigate à interface GridProps

Como antes, você adiciona uma propriedade de retorno de chamada adicional ao componente Grid adicionando o seguinte na interface GridProps dentro de Grid.tsx:

export interface GridProps {
  width?: number;
  height?: number;
  columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
  records: Record<
    string,
    ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
  >;
  sortedRecordIds: string[];
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  totalResultCount: number;
  currentPage: number;
  sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
  filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
  resources: ComponentFramework.Resources;
  itemsLoading: boolean;
  highlightValue: string | null;
  highlightColor: string | null;
  setSelectedRecords: (ids: string[]) => void;
}

Adicionar onNavigate a adereços de grade

Novamente, você deve adicionar o novo adereço à destruidora dos adereços:

export const Grid = React.memo((props: GridProps) => {
  const {
    records,
    sortedRecordIds,
    columns,
    width,
    height,
    hasNextPage,
    hasPreviousPage,
    sorting,
    filtering,
    currentPage,
    itemsLoading,
    setSelectedRecords,
  } = props;

Adicionar onItemInvoked ao DetailsList

O DetailList tem um adereço de retorno de chamada chamado onItemInvoked que, por sua vez, você passa o retorno de chamada para:

<DetailsList
   columns={gridColumns}
   onRenderItemColumn={onRenderItemColumn}
   onRenderDetailsHeader={onRenderDetailsHeader}
   items={items}
   setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
   initialFocusedIndex={0}
   checkButtonAriaLabel="select row"
   layoutMode={DetailsListLayoutMode.fixedColumns}
   constrainMode={ConstrainMode.unconstrained}
   selection={selection}
></DetailsList>

Adicionar o método onNavigate ao index.ts

Adicione o onNavigate método ao index.ts logo abaixo do setSelectedRecords método:

onNavigate = (
  item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
): void => {
  if (item) {
    this.context.parameters.records.openDatasetItem(item.getNamedReference());
  }
};

Isso simplesmente invoca o openDatasetItem método no registro do conjunto de dados para que o componente de código acione o OnSelect evento. O método é definido como uma função de seta para associá-lo à instância atual this do componente de código.

Você precisa passar essa ação de retorno para as propriedades do componente Grid dentro do método updateView.

    ReactDOM.render(
      React.createElement(Grid, {
        width: allocatedWidth,
        height: allocatedHeight,
        columns: dataset.columns,
        records: this.records,
        sortedRecordIds: this.sortedRecordsIds,
        hasNextPage: paging.hasNextPage,
        hasPreviousPage: paging.hasPreviousPage,
        currentPage: this.currentPage,
        totalResultCount: paging.totalResultCount,
        sorting: dataset.sorting,
        filtering: dataset.filtering && dataset.filtering.getFilter(),
        resources: this.resources,
        itemsLoading: dataset.loading,
        highlightValue: this.context.parameters.HighlightValue.raw,
        highlightColor: this.context.parameters.HighlightColor.raw,
        setSelectedRecords: this.setSelectedRecords,
      }),
      this.container
    );

Quando você salvar todos os arquivos, o ambiente de teste recarregará. Use Ctrl + Shift + I(ou F12) e use Abrir Arquivo (Ctrl + P) pesquisando index.ts e você pode colocar um ponto de interrupção dentro do método onNavigate. Clicar duas vezes em uma linha (ou realçá-la com as teclas de cursor e pressionar Enter) atingirá o ponto de interrupção porque DetailsList invoca o retorno de chamada onNavigate.

Depurar OnNavigate no Canvas Data Grid em index.ts

Há uma referência a _this porque a função é definida como uma função arrow e foi transpilada em um closure JavaScript para capturar a instância de this.

Adicionar Localização

Antes de ir mais longe, você precisa adicionar cadeias de caracteres de recurso ao componente de código para que você possa usar cadeias de caracteres localizadas para mensagens como paginação, classificação e filtragem. Adicione um novo arquivo CanvasGrid\strings\CanvasGrid.1033.resx e use o editor de recursos Visual Studio ou Visual Studio Code com uma extensão para inserir o seguinte:

Nome Valor
Records_Dataset_Display Arquivos
FilteredRecordCount_Disp Contagem de registros filtrados
FilteredRecordCount_Desc O número de registros após a filtragem
HighlightValue_Disp Realçar valor
HighlightValue_Desc O valor para indicar uma linha deve ser realçado
HighlightColor_Disp Cor do Realce
HighlightColor_Desc A cor para realçar uma linha usando
HighlightIndicator_Disp Campo Indicador de Destaque
HighlightIndicator_Desc Configurar o nome do campo para ser comparado com o Valor de Destaque
Label_Grid_Footer Página {0} ({1} Selecionada)
Label_SortAZ De A a Z
Label_SortZA Z para A
Label_DoesNotContainData Não contém dados
Label_ShowFullScreen Mostrar Tela Inteira

Dica

Não é recomendável editar resx arquivos diretamente. Em vez disso, use o editor de recursos do Visual Studio ou uma extensão para Visual Studio Code. Encontre uma extensão Visual Studio Code: Pesquise no Visual Studio Marketplace por um editor resx

Os dados desse arquivo também podem ser definidos abrindo o CanvasGrid.1033.resx arquivo no Bloco de Notas e copiando o conteúdo XML abaixo:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="Records_Dataset_Display" xml:space="preserve">
    <value>Records</value>
  </data>
  <data name="FilteredRecordCount_Disp" xml:space="preserve">
    <value>Filtered Record Count</value>
  </data>
  <data name="FilteredRecordCount_Desc" xml:space="preserve">
    <value>The number of records after filtering</value>
  </data>
  <data name="HighlightValue_Disp" xml:space="preserve">
    <value>Highlight Value</value>
  </data>
  <data name="HighlightValue_Desc" xml:space="preserve">
    <value>The value to indicate a row should be highlighted</value>
  </data>
  <data name="HighlightColor_Disp" xml:space="preserve">
    <value>Highlight Color</value>
  </data>
  <data name="HighlightColor_Desc" xml:space="preserve">
    <value>The color to highlight a row using</value>
  </data>
  <data name="HighlightIndicator_Disp" xml:space="preserve">
    <value>Highlight Indicator Field</value>
  </data>
  <data name="HighlightIndicator_Desc" xml:space="preserve">
    <value>Set to the name of the field to compare against the Highlight Value</value>
  </data>
   <data name="Label_Grid_Footer" xml:space="preserve">
    <value>Page {0} ({1} Selected)</value>
  </data>
  <data name="Label_SortAZ" xml:space="preserve">
    <value>A to Z</value>
  </data>
  <data name="Label_SortZA" xml:space="preserve">
    <value>Z to A</value>
  </data>
  <data name="Label_DoesNotContainData" xml:space="preserve">
    <value>Does not contain data</value>
  </data>
  <data name="Label_ShowFullScreen" xml:space="preserve">
    <value>Show Full Screen</value>
  </data>
</root>

Você tem cadeias de caracteres de recurso para as propriedades input, /, output e as dataset associadas a property-set. Elas serão usadas no Power Apps Studio no momento de design com base na linguagem do navegador do criador. Você também pode adicionar cadeias de caracteres de rótulo que podem ser recuperadas em runtime usando getString. Mais informações: Implementando o componente de API de localização.

Adicione este novo arquivo de recurso ao ControlManifest.Input.xml arquivo dentro do resources elemento:

<resources>
   <code path="index.ts"
      order="1" />
</resources>

Adicionar classificação e filtragem de colunas

Se você quiser permitir que o usuário classifique e filtre usando cabeçalhos de coluna de grade, o Fluent UI DetailList fornece uma maneira fácil de adicionar menus de contexto aos cabeçalhos de coluna.

Adicionar onSort e onFilter a GridProps

Primeiro, adicione onSort e onFilter à interface GridProps dentro de Grid.tsx para fornecer funções de retorno de chamada para classificação e filtragem.

export interface GridProps {
  width?: number;
  height?: number;
  columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
  records: Record<
    string,
    ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
  >;
  sortedRecordIds: string[];
  hasNextPage: boolean;
  hasPreviousPage: boolean;
  totalResultCount: number;
  currentPage: number;
  sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
  filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
  resources: ComponentFramework.Resources;
  itemsLoading: boolean;
  highlightValue: string | null;
  highlightColor: string | null;
  setSelectedRecords: (ids: string[]) => void;
  onNavigate: (
    item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord
  ) => void;
}

Adicionar onSort, onFilter e recursos a adereços

Em seguida, adicione esses novos adereços junto com a resources referência (para que você possa recuperar rótulos localizados para classificação e filtragem) aos adereços que se destruem:

export const Grid = React.memo((props: GridProps) => {
  const {
    records,
    sortedRecordIds,
    columns,
    width,
    height,
    hasNextPage,
    hasPreviousPage,
    sorting,
    filtering,
    currentPage,
    itemsLoading,
    setSelectedRecords,
    onNavigate,
  } = props;

Importar componentes ContextualMenu

Você precisa adicionar algumas importações ao início de Grid.tsx para que você possa usar o componente ContextualMenu fornecido pelo Fluent UI. Você pode usar importações baseadas em caminho para reduzir o tamanho do pacote.

import { ContextualMenu, DirectionalHint, IContextualMenuProps } from '@fluentui/react/lib/ContextualMenu';

Adicionar funcionalidade de renderização do menu de contexto

Agora, adicione a funcionalidade de renderização do menu de contexto no Grid.tsx, logo abaixo da linha
const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);:

const [isComponentLoading, setIsLoading] = React.useState<boolean>(false);

Você verá isso:

  • O contextualMenuProps estado controla a visibilidade do menu de contexto renderizado usando o componente Fluent UI ContextualMenu.
  • Esse código fornece um filtro simples para mostrar apenas valores em que o campo não contém dados. Você pode estender isso para fornecer filtragem adicional.
  • Esse código usa resources.getString para mostrar rótulos no menu de contexto que podem ser localizados.
  • O React.useCallback gancho, semelhante a React.useMemo, garante que os retornos de chamada sejam modificados apenas quando os valores dependentes forem alterados. Isso otimiza a renderização de componentes filho.

Adicionar novas funções de menu de contexto aos eventos de seleção de coluna e de menu de contexto

Adicione essas novas funções de menu de contexto aos eventos de seleção de coluna e de menu de contexto. Atualize o const gridColumns para adicionar os retornos de chamada onColumnContextMenu e onColumnClick.

const gridColumns = React.useMemo(() => {
   return columns
     .filter((col) => !col.isHidden && col.order >= 0)
     .sort((a, b) => a.order - b.order)
     .map((col) => {
       const sortOn = sorting && sorting.find((s) => s.name === col.name);
       const filtered =
         filtering &&
         filtering.conditions &&
         filtering.conditions.find((f) => f.attributeName == col.name);
       return {
         key: col.name,
         name: col.displayName,
         fieldName: col.name,
         isSorted: sortOn != null,
         isSortedDescending: sortOn?.sortDirection === 1,
         isResizable: true,
         isFiltered: filtered != null,
         data: col,
       } as IColumn;
     });
 }, [columns, sorting]);

Adicionar menu de contexto à saída renderizada

Para que o menu de contexto seja mostrado, você precisa adicioná-lo à saída renderizada. Adicione o seguinte diretamente abaixo do componente DetailsList na saída retornada:

<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
    <DetailsList
      columns={gridColumns}
      onRenderItemColumn={onRenderItemColumn}
      onRenderDetailsHeader={onRenderDetailsHeader}
      items={items}
      setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
      initialFocusedIndex={0}
      checkButtonAriaLabel="select row"
      layoutMode={DetailsListLayoutMode.fixedColumns}
      constrainMode={ConstrainMode.unconstrained}
      selection={selection}
      onItemInvoked={onNavigate}
    ></DetailsList>
</ScrollablePane>

Adicionar funções onSort e OnFilter

Agora que você adicionou a interface do usuário de classificação e filtragem, você precisa adicionar os callbacks para index.ts para realmente realizar a classificação e filtragem nos registros vinculados ao componente de código. Adicione o seguinte logo abaixo de index.ts, à função onNavigate:

onSort = (name: string, desc: boolean): void => {
  const sorting = this.context.parameters.records.sorting;
  while (sorting.length > 0) {
    sorting.pop();
  }
  this.context.parameters.records.sorting.push({
    name: name,
    sortDirection: desc ? 1 : 0,
  });
  this.context.parameters.records.refresh();
};

onFilter = (name: string, filter: boolean): void => {
  const filtering = this.context.parameters.records.filtering;
  if (filter) {
    filtering.setFilter({
      conditions: [
        {
          attributeName: name,
          conditionOperator: 12, // Does not contain Data
        },
      ],
    } as ComponentFramework.PropertyHelper.DataSetApi.FilterExpression);
  } else {
    filtering.clearFilter();
  }
  this.context.parameters.records.refresh();
};

Você verá isso:

  • A classificação e o filtro são aplicados ao conjunto de dados usando as propriedades de classificação e filtragem .
  • Ao modificar as colunas de classificação, as definições de classificação existentes devem ser removidas usando pop em vez de substituir a própria matriz de classificação.
  • A atualização deve ser chamada após a classificação e a filtragem ser aplicada. Se um filtro e uma classificação forem aplicados ao mesmo tempo, a atualização só precisará ser chamada uma vez.

Adicionar callbacks OnSort e OnFilter à renderização do Grid

Por fim, você pode passar essas duas funções de retorno para a Grid chamada de renderização.

ReactDOM.render(
    React.createElement(Grid, {
        width: allocatedWidth,
        height: allocatedHeight,
        columns: dataset.columns,
        records: this.records,
        sortedRecordIds: this.sortedRecordsIds,
        hasNextPage: paging.hasNextPage,
        hasPreviousPage: paging.hasPreviousPage,
        currentPage: this.currentPage,
        totalResultCount: paging.totalResultCount,
        sorting: dataset.sorting,
        filtering: dataset.filtering && dataset.filtering.getFilter(),
        resources: this.resources,
        itemsLoading: dataset.loading,
        highlightValue: this.context.parameters.HighlightValue.raw,
        highlightColor: this.context.parameters.HighlightColor.raw,
        setSelectedRecords: this.setSelectedRecords,
        onNavigate: this.onNavigate,
    }),
    this.container
);

Observação

Neste ponto, você não pode mais testar o uso do cinto de teste porque ele não fornece suporte para classificação e filtragem. Posteriormente, você pode implantar usando o pac pcf push e depois adicionar a um aplicativo canvas para teste. Se desejar, você pode pular para essa etapa para ver como o componente de código fica dentro de aplicativos canvas.

Atualizar a propriedade de saída FilteredRecordCount

Como a grid agora pode filtrar registros internamente, é importante informar ao aplicativo canvas quantos registros são exibidos. Isso é para que você possa mostrar uma mensagem de tipo "Sem Registros".

Dica

Você pode implementar isso internamente no componente de código; no entanto, é recomendável que o máximo da interface do usuário seja implementado no aplicativo canvas, pois isso dará ao criador mais flexibilidade.

Você já definiu uma propriedade de saída chamada FilteredRecordCount no ControlManifest.Input.xml. Quando a filtragem ocorrer e os registros filtrados forem carregados, a updateView função será chamada com cadeia de caracteres dataset na matriz updatedProperties . Se o número de registros tiver sido alterado, você precisará fazer uma chamada para notifyOutputChanged para que o aplicativo canvas saiba que deve atualizar todos os controles que usam a propriedade FilteredRecordCount. Dentro do updateView método de index.ts, adicione o seguinte logo acima do ReactDOM.render e abaixo allocatedHeight:

const allocatedHeight = parseInt(
    context.mode.allocatedHeight as unknown as string
);

Adicionar FilteredRecordCount a getOutputs

Isso atualiza a filteredRecordCount classe de componente de código que você definiu anteriormente quando ela é diferente dos novos dados recebidos. Depois que notifyOutputChanged for chamado, você precisa garantir que o valor seja retornado quando getOutputs for chamado, então atualize o método getOutputs para ser:

public getOutputs(): IOutputs {
    return {};
}

Adicionar paginação à grade

Para grandes conjuntos de dados, os aplicativos de tela dividirão os registros em várias páginas. Você pode adicionar um rodapé que mostra os controles de navegação de página. Cada botão será renderizado usando o Fluent UI IconButton, que você deve importar.

Adicionar IconButton às importações

Adicione isso às importações dentro Grid.tsx:

import { IconButton } from '@fluentui/react/lib/Button';

Adicionar função stringFormat

A etapa a seguir adicionará capacidades para carregar o formato do rótulo do indicador de página a partir das cadeias de caracteres de recursos ("Page {0} ({1} Selected)") e formatar usando uma função simples stringFormat. Essa função também pode estar em um arquivo separado e compartilhada entre seus componentes por conveniência:

Neste tutorial, adicione-o na parte superior de Grid.tsx, logo abaixo type DataSet ....

function stringFormat(template: string, ...args: string[]): string {
  for (const k in args) {
    template = template.replace("{" + k + "}", args[k]);
  }
  return template;
}

Adicionar botões de paginação

In Grid.tsx, adicione o seguinte Stack.Item abaixo do existente Stack.Item que contém o ScrollablePane:

return (
  <Stack verticalFill grow style={rootContainerStyle}>
      <Stack.Item grow style={{ position: 'relative', backgroundColor: 'white' }}>
        <ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
            <DetailsList
              columns={gridColumns}
              onRenderItemColumn={onRenderItemColumn}
              onRenderDetailsHeader={onRenderDetailsHeader}
              items={items}
              setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
              initialFocusedIndex={0}
              checkButtonAriaLabel="select row"
              layoutMode={DetailsListLayoutMode.fixedColumns}
              constrainMode={ConstrainMode.unconstrained}
              selection={selection}
              onItemInvoked={onNavigate}
            ></DetailsList>
            {contextualMenuProps && <ContextualMenu {...contextualMenuProps} />}
        </ScrollablePane>
        {(itemsLoading || isComponentLoading) && <Overlay />}
      </Stack.Item>    
  </Stack>
);

Você verá isso:

  • O Stack garante que o rodapé ficará posicionado abaixo do DetailsList. O grow atributo é usado para garantir que a grade se expanda para preencher o espaço disponível.
  • Você carrega o formato do rótulo de indicador de página das cadeias de caracteres de recurso ("Page {0} ({1} Selected)") e formata usando a função stringFormat que você adicionou na etapa anterior.
  • Você pode fornecer alt texto para acessibilidade na paginação IconButtons.
  • O estilo no rodapé também pode ser aplicado usando um nome de classe CSS referenciando um arquivo CSS adicionado ao componente de código.

Adicionar adereços de retorno de chamada para dar suporte à paginação

Em seguida, você deve adicionar as propriedades ausentes loadFirstPage, loadNextPage e loadPreviousPage de retorno de chamada.

Para a GridProps interface, adicione o seguinte:

export interface GridProps {
   width?: number;
   height?: number;
   columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
   records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
   sortedRecordIds: string[];
   hasNextPage: boolean;
   hasPreviousPage: boolean;
   totalResultCount: number;
   currentPage: number;
   sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
   filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
   resources: ComponentFramework.Resources;
   itemsLoading: boolean;
   highlightValue: string | null;
   highlightColor: string | null;
   setSelectedRecords: (ids: string[]) => void;
   onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
   onSort: (name: string, desc: boolean) => void;
   onFilter: (name: string, filtered: boolean) => void;
}

Adicionar novas propriedades de paginação ao Grid

Adicione estes novos adereços à destruidora de adereços:

export const Grid = React.memo((props: GridProps) => {
   const {
      records,
      sortedRecordIds,
      columns,
      width,
      height,
      hasNextPage,
      hasPreviousPage,
      sorting,
      filtering,
      currentPage,
      itemsLoading,
      setSelectedRecords,
      onNavigate,
      onSort,
      onFilter,
      resources,
   } = props;

Adicionar retornos de chamada ao index.ts

Adicione esses retornos de chamada ao index.ts método abaixo onFilter :

loadFirstPage = (): void => {
  this.currentPage = 1;
  this.context.parameters.records.paging.loadExactPage(1);
};
loadNextPage = (): void => {
  this.currentPage++;
  this.context.parameters.records.paging.loadExactPage(this.currentPage);
};
loadPreviousPage = (): void => {
  this.currentPage--;
  this.context.parameters.records.paging.loadExactPage(this.currentPage);
};

Em seguida, atualize a Grid chamada de renderização para incluir estes retornos de chamada:

ReactDOM.render(
    React.createElement(Grid, {
        width: allocatedWidth,
        height: allocatedHeight,
        columns: dataset.columns,
        records: this.records,
        sortedRecordIds: this.sortedRecordsIds,
        hasNextPage: paging.hasNextPage,
        hasPreviousPage: paging.hasPreviousPage,
        currentPage: this.currentPage,
        totalResultCount: paging.totalResultCount,
        sorting: dataset.sorting,
        filtering: dataset.filtering && dataset.filtering.getFilter(),
        resources: this.resources,
        itemsLoading: dataset.loading,
        highlightValue: this.context.parameters.HighlightValue.raw,
        highlightColor: this.context.parameters.HighlightColor.raw,
        setSelectedRecords: this.setSelectedRecords,
        onNavigate: this.onNavigate,
        onSort: this.onSort,
        onFilter: this.onFilter,
    }),
    this.container
);

Adicionar suporte de tela inteira

Os componentes de código oferecem a capacidade de mostrar no modo de tela inteira. Isso é especialmente útil em tamanhos de tela pequenos ou onde há espaço limitado para o componente de código em uma tela de aplicativo canvas.

Para iniciar modo de tela inteira, você pode usar o componente Fluent UI Link. Adicione-o às importações na parte superior de Grid.tsx:

import { Link } from '@fluentui/react/lib/Link';

Para adicionar um link de tela inteira, adicione o seguinte ao existente Stack que contém os controles de paginação.

Observação

Certifique-se de adicionar isso ao elemento aninhado Stack, e não ao elemento raiz Stack.

<Stack horizontal style={{ width: '100%', paddingLeft: 8, paddingRight: 8 }}>
    <IconButton
      alt="First Page"
      iconProps={{ iconName: 'Rewind' }}
      disabled={!hasPreviousPage}
      onClick={loadFirstPage}
    />
    <IconButton
      alt="Previous Page"
      iconProps={{ iconName: 'Previous' }}
      disabled={!hasPreviousPage}
      onClick={loadPreviousPage}
    />
    <Stack.Item align="center">
      {stringFormat(
          resources.getString('Label_Grid_Footer'),
          currentPage.toString(),
          selection.getSelectedCount().toString(),
      )}
    </Stack.Item>
    <IconButton
      alt="Next Page"
      iconProps={{ iconName: 'Next' }}
      disabled={!hasNextPage}
      onClick={loadNextPage}
    />
</Stack>

Você verá isso:

  • Esse código usa recursos para mostrar o rótulo para dar suporte à localização.
  • Se o modo de tela inteira estiver aberto, o link não será mostrado. Em vez disso, o contexto do aplicativo pai renderiza automaticamente um ícone de fechamento.

Adicionar adereços para dar suporte a tela inteira a GridProps

Adicione os props onFullScreen e isFullScreen à interface GridProps dentro de Grid.tsx para fornecer callbacks para classificação e filtragem.

export interface GridProps {
   width?: number;
   height?: number;
   columns: ComponentFramework.PropertyHelper.DataSetApi.Column[];
   records: Record<string, ComponentFramework.PropertyHelper.DataSetApi.EntityRecord>;
   sortedRecordIds: string[];
   hasNextPage: boolean;
   hasPreviousPage: boolean;
   totalResultCount: number;
   currentPage: number;
   sorting: ComponentFramework.PropertyHelper.DataSetApi.SortStatus[];
   filtering: ComponentFramework.PropertyHelper.DataSetApi.FilterExpression;
   resources: ComponentFramework.Resources;
   itemsLoading: boolean;
   highlightValue: string | null;
   highlightColor: string | null;
   setSelectedRecords: (ids: string[]) => void;
   onNavigate: (item?: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord) => void;
   onSort: (name: string, desc: boolean) => void;
   onFilter: (name: string, filtered: boolean) => void;
   loadFirstPage: () => void;
   loadNextPage: () => void;
   loadPreviousPage: () => void;
}

Adicionar adereços para dar suporte à tela inteira à Grade

Adicione estes novos adereços à destruidora de adereços:

export const Grid = React.memo((props: GridProps) => {
   const {
      records,
      sortedRecordIds,
      columns,
      width,
      height,
      hasNextPage,
      hasPreviousPage,
      sorting,
      filtering,
      currentPage,
      itemsLoading,
      setSelectedRecords,
      onNavigate,
      onSort,
      onFilter,
      resources,
      loadFirstPage,
      loadNextPage,
      loadPreviousPage,
   } = props;

Atualizar index.ts para dar suporte à tela inteira para a Grade

Para fornecer esses novos props, dentro de index.ts, adicione o seguinte método callback abaixo de loadPreviousPage:

onFullScreen = (): void => {
  this.context.mode.setFullScreen(true);
};

A chamada para setFullScreen faz com que o componente de código abra o modo de tela inteira e ajuste allocatedHeight e allocatedWidth adequadamente por causa da chamada para trackContainerResize(true) no método init. Depois que o modo de tela inteira estiver aberto, updateView será chamado, atualizando a renderização do componente com o novo tamanho. O updatedProperties contém fullscreen_open ou fullscreen_close, dependendo da transição que está acontecendo.

Para armazenar o estado do modo de tela inteira, adicione um novo isFullScreen campo à CanvasGrid classe dentro index.ts:

export class CanvasGrid implements ComponentFramework.StandardControl<IInputs, IOutputs> {
    notifyOutputChanged: () => void;
    container: HTMLDivElement;
    context: ComponentFramework.Context<IInputs>;
    sortedRecordsIds: string[] = [];
    resources: ComponentFramework.Resources;
    isTestHarness: boolean;
    records: {
        [id: string]: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
    };
    currentPage = 1;
    filteredRecordCount?: number;

Editar updateView para acompanhar o estado

Adicione o seguinte ao updateView método para acompanhar o estado:

public updateView(context: ComponentFramework.Context<IInputs>): void {
    const dataset = context.parameters.records;
    const paging = context.parameters.records.paging;
    const datasetChanged = context.updatedProperties.indexOf("dataset") > -1;
    const resetPaging =
        datasetChanged &&
        !dataset.loading &&
        !dataset.paging.hasPreviousPage &&
        this.currentPage !== 1;

    if (resetPaging) {
        this.currentPage = 1;
    }

Passe o callback e o campo isFullScreen para renderizar no Grid

Agora você pode passar o callback e o campo isFullScreen para as propriedades de renderização Grid.

ReactDOM.render(
    React.createElement(Grid, {
        width: allocatedWidth,
        height: allocatedHeight,
        columns: dataset.columns,
        records: this.records,
        sortedRecordIds: this.sortedRecordsIds,
        hasNextPage: paging.hasNextPage,
        hasPreviousPage: paging.hasPreviousPage,
        currentPage: this.currentPage,
        totalResultCount: paging.totalResultCount,
        sorting: dataset.sorting,
        filtering: dataset.filtering && dataset.filtering.getFilter(),
        resources: this.resources,
        itemsLoading: dataset.loading,
        highlightValue: this.context.parameters.HighlightValue.raw,
        highlightColor: this.context.parameters.HighlightColor.raw,
        setSelectedRecords: this.setSelectedRecords,
        onNavigate: this.onNavigate,
        onSort: this.onSort,
        onFilter: this.onFilter,
        loadFirstPage: this.loadFirstPage,
        loadNextPage: this.loadNextPage,
        loadPreviousPage: this.loadPreviousPage,
    }),
    this.container
);

Realçando linhas

Agora você está pronto para adicionar a funcionalidade de destaque de linha condicional. Você já definiu as HighlightValue e HighlightColor propriedades de entrada, e as HighlightIndicatorproperty-set. "O property-set permite que o criador escolha um campo para comparar com o valor fornecido em HighlightValue."

Importar tipos para dar suporte ao realce

A renderização de linha personalizada no DetailsList requer algumas importações adicionais. Já existem alguns tipos de @fluentui/react/lib/DetailsList, então adicione IDetailsListProps, IDetailsRowStyles e DetailsRow a essa instrução de importação em Grid.tsx:

import {
    DetailsList,
    ConstrainMode,
    DetailsListLayoutMode,
    IColumn,
    IDetailsHeaderProps
} from '@fluentui/react/lib/DetailsList';

Agora, crie o renderizador de linha personalizado adicionando o seguinte logo abaixo do const rootContainerStyle bloco:

const onRenderRow: IDetailsListProps['onRenderRow'] = (props) => {
    const customStyles: Partial<IDetailsRowStyles> = {};
    if (props && props.item) {
        const item = props.item as DataSet | undefined;
        if (highlightColor && highlightValue && item?.getValue('HighlightIndicator') == highlightValue) {
            customStyles.root = { backgroundColor: highlightColor };
        }
        return <DetailsRow {...props} styles={customStyles} />;
    }
    return null;
};

Você verá isso:

  • Você pode recuperar o valor do campo escolhido pelo criador por meio do alias HighlightIndicator usando:
    item?.getValue('HighlightIndicator').
  • Quando o valor do campo HighlightIndicator corresponde ao valor fornecido pela propriedade de entrada highlightValue no componente de código, você pode adicionar uma cor de plano de fundo à linha.
  • O DetailsRow componente é usado pela DetailsList para renderizar as colunas que você definiu. Você não precisa alterar o comportamento que não seja a cor da tela de fundo.

Adicionar adereços adicionais para dar suporte ao realce

Adicione algumas propriedades adicionais para highlightColor e highlightValue que serão fornecidas pela renderização dentro de updateView. Você já adicionou à GridProps interface, então você só precisa adicioná-los à desestruturação de props.

export const Grid = React.memo((props: GridProps) => {
   const {
      records,
      sortedRecordIds,
      columns,
      width,
      height,
      hasNextPage,
      hasPreviousPage,
      sorting,
      filtering,
      currentPage,
      itemsLoading,
      setSelectedRecords,
      onNavigate,
      onSort,
      onFilter,
      resources,
      loadFirstPage,
      loadNextPage,
      loadPreviousPage,
      onFullScreen, 
      isFullScreen,
   } = props;

Adicionar o método onRenderRow à DetailsList

Passe o método onRenderRow nas propriedades DetailsList.

<DetailsList
  columns={gridColumns}
  onRenderItemColumn={onRenderItemColumn}
  onRenderDetailsHeader={onRenderDetailsHeader}
  items={items}
  setKey={`set${currentPage}`} // Ensures that the selection is reset when paging
  initialFocusedIndex={0}
  checkButtonAriaLabel="select row"
  layoutMode={DetailsListLayoutMode.fixedColumns}
  constrainMode={ConstrainMode.unconstrained}
  selection={selection}
  onItemInvoked={onNavigate}
></DetailsList>

Implantar e configurar o componente

Agora que você implementou todos os recursos, deve implantar o componente de código no Microsoft Dataverse para testes.

  1. Dentro do ambiente do Dataverse, verifique se há um publicador criado com um prefixo de samples:

    Adicionar novo publicador

    Esse também pode ser seu próprio publicador, desde que você atualize o parâmetro de prefixo do publicador ao chamar pac pcf push abaixo. Mais informações: criar um editor de soluções.

  2. Depois de salvar o publisher, você estará pronto para autorizar a CLI no seu ambiente e assim possamos enviar o componente de código compilado. Na linha de comando, use:

    pac auth create --url https://myorg.crm.dynamics.com
    

    Substitua myorg.crm.dynamics.com pela URL do seu próprio ambiente do Dataverse. Entre como um usuário administrador/personalizador quando solicitado. Os privilégios fornecidos por essas funções de usuário são necessários para implantar quaisquer componentes de código no Dataverse.

  3. Para implantar o componente de código, use:

    pac pcf push --publisher-prefix samples
    

    Observação

    Se você receber o erro , deverá instalar o Visual Studio 2019 para Windows & Mac ou o Build Tools for Visual Studio 2019, certificando-se de selecionar a carga de trabalho 'ferramentas de build .NET', conforme descrito nos pré-requisitos.

  4. Depois de concluído, esse processo terá criado uma pequena solução temporária chamada PowerAppTools_samples em seu ambiente e o CanvasGrid componente de código será adicionado a essa solução. Você pode mover o componente de código para sua própria solução mais tarde, se necessário. Mais informações: ALM (Gerenciamento do Ciclo de Vida do Aplicativo de Componente de Código).

    solução PowerAppsTools_samples

  5. Para usar componentes de código dentro de aplicativos de tela, você deve habilitar a estrutura de componentes Power Apps para aplicativos de tela no ambiente que você está usando.

    a. Abra o Centro de Administração (admin.powerplatform.microsoft.com) e navegue até seu ambiente. b. Navegue até Configurações>Produto>Recursos. Certifique-se de que a estrutura de componentes do Power Apps para aplicativos de canvas esteja Ligada:

    Habilitar componentes de código

  6. Crie um novo aplicativo Canvas usando o layout do Tablet.

  7. No painel Inserir , selecione Obter mais componentes.

  8. Selecione a guia Código no painel Importar componentes .

  9. Selecione o componente CanvasGrid.

  10. Selecione Importar. O componente de código agora aparecerá em componentes de código no painel Inserir .

  11. Arraste o componente CanvasGrid para a tela e associe à tabela Contacts em Microsoft Dataverse.

  12. Defina as seguintes propriedades no componente de código CanvasGrid usando o painel de propriedades.

    • Realçar valor = 1 - Esse é o valor que statecode tem quando o registro está inativo.
    • Cor de Realce = #FDE7E9 - Essa é a cor a ser usada quando o registro está inativo.
    • HighlightIndicator = "statecode" - Esse é o campo com o qual comparar. Isso estará no painel Avançado na seção DATA .

    Painel de Propriedades

  13. Adicione um novo TextInput componente e nomeie-o txtSearch.

  14. Atualize a CanvasGrid.Items propriedade para ser Search(Contacts,txtSearch.Text,"fullname").

    Ao digitar na Entrada de Texto, você verá que os contatos são filtrados na tabela.

  15. Adicione um novo rótulo de texto e defina o texto como "Nenhum registro encontrado". Posicione o rótulo acima da Grade do Canvas.

  16. Defina a propriedade Visible do rótulo Text como CanvasGrid1.FilteredRecordCount=0.

Isso significa que, quando não houver registros que correspondam ao txtSearch valor ou se um filtro de coluna for aplicado usando o menu de contexto que não retorna registros (por exemplo, Nome Completo não contém dados), o rótulo será exibido.

  1. Adicione um Formulário de Exibição (do grupo de entrada no painel Inserir ).

  2. Defina o formulário DataSource para a Contacts tabela e adicione alguns campos de formulário.

  3. Defina a propriedade do formulário Item como CanvasGrid1.Selected.

    Agora você deverá ver que, quando selecionar itens na grade, o formulário exibirá o item selecionado.

  4. Adicione uma nova Tela ao aplicativo de tela chamado scrDetails.

  5. Copie o formulário da tela anterior e cole-o na nova tela.

  6. Defina a CanvasGrid1.OnSelect propriedade como .Navigate(scrDetails)

    Quando você invoca a ação de seleção de linha da grade, agora você verá que o aplicativo navega para a segunda tela com o item selecionado.

Depuração após a implantação

Você pode depurar facilmente seu componente de código em execução dentro do aplicativo canvas ao abrir as Ferramentas de Desenvolvedor usando Ctrl+Shift+I.

Selecione e digite Ctrl+PGrid.tsx ou Index.ts. Em seguida, você pode definir um ponto de parada e passar passo a passo pelo seu código.

Depurar aplicativos Canvas

Se você precisar fazer mais alterações em seu componente, não precisará implantar a cada vez. Em vez disso, use a técnica descrita em Debug code components para criar um Fiddler AutoResponder para carregar o arquivo do sistema de arquivos local enquanto npm start watch está em execução.

O AutoResponder seria semelhante ao seguinte:

REGEX:(.*?)((?'folder'css|html)(%252f|\/))?SampleNamespace\.CanvasGrid[\.\/](?'fname'[^?]*\.*)(.*?)$
C:\repos\CanvasGrid\out\controls\CanvasGrid\${folder}\${fname}

Regra de Auto Responder

Você também precisará habilitar os filtros para adicionar o Access-Control-Allow-Origin cabeçalho. Mais informações: Debugging após a implantação em Microsoft Dataverse.

Você precisará esvaziar o cache e atualizar a sessão do navegador para que o arquivo AutoResponder seja coletado. Depois de carregado, você pode simplesmente atualizar o navegador, pois o Fiddler adicionará um cabeçalho de controle de cache ao arquivo para impedir que ele seja armazenado em cache.

Quando estiver satisfeito com as alterações, você poderá incrementar a versão do patch no manifesto e reimplantar usando o pac pcf push.

Até agora, você implantou um build de desenvolvimento, que não é otimizado e será executado mais lento no runtime. Você pode optar por implantar um build otimizado usando o pac pcf push editando o CanvasGrid.pcfproj. Abaixo do OutputPath, adicione o seguinte: <PcfBuildMode>production</PcfBuildMode>

  <PropertyGroup>
    <Name>CanvasGrid</Name>
    <ProjectGuid>a670bba8-e0ae-49ed-8cd2-73917bace346</ProjectGuid>
    <OutputPath>$(MSBuildThisFileDirectory)out\controls</OutputPath>
  </PropertyGroup>

Gerenciamento do ciclo de vida de aplicativos (ALM) com Microsoft Power Platform
Referência da API da estrutura de componentes do Power Apps
Criar seu primeiro componente
Depurar componentes de código