# Build PCF Controls Skill

Guide the user through building, testing, and deploying PowerApps Component Framework (PCF) controls for Model-Driven Apps, including binding them to form fields via MCP tools.

## Overview

PCF controls are custom UI components built with TypeScript/React that replace standard Dataverse form controls (e.g., turn a plain dropdown into a visual progress bar). They run client-side in the browser and are deployed via solution import, then bound to fields via formxml.

## Pre-Flight

Before building a PCF control:
- Confirm the target environment, solution name, and publisher prefix (if first operation this session)
- Query the target table and form to understand the field the control will bind to: `dataverse_get_column` for field type, `dataverse_get_form` for current form layout
- Check if a PCF control already exists in the solution for this purpose
- Verify the development environment has the PCF CLI (`pac pcf init`) and npm available

## Guardrails

- **Manifest field type must match the bound column type.** A control bound to a `SingleLine.Text` column must declare `<type-group>` with `SingleLine.Text` in the manifest. Mismatches cause silent binding failures.
- **formxml binding requires both `uniqueid` and `controlDescriptions`.** Missing either causes the control to not render on the form. Always use the full binding pattern.
- **Solution ZIP is required for deployment.** PCF controls cannot be registered via individual file upload — they must be packaged into a solution ZIP and imported via `dataverse_import_solution`.
- **Test locally before deploying.** Run `npm start` to verify the control renders correctly in the test harness before packaging.

## Prerequisites

- **pac CLI** — install via `winget install Microsoft.PowerAppsCLI`. Located at `%LOCALAPPDATA%\Microsoft\PowerAppsCli\pac.cmd`
- **Node.js** — for npm install/build
- **dotnet SDK** — for solution packaging (`dotnet build`)

## Project Setup

### Initialize PCF Project

```bash
# Create a new PCF control project
mkdir MyPCFControl && cd MyPCFControl
pac pcf init --namespace MyCompany --name MyControl --template field --framework react --run-npm-install
# Templates: "field" (bound to a column) or "dataset" (bound to a view/subgrid)
# Frameworks: "react" (recommended) or "none" (vanilla TS)
```

### Project Structure

```
MyPCFControl/
├── ControlManifest.Input.xml    # Control metadata, properties, resources
├── index.ts                     # Main control class (IInputs/IOutputs)
├── MyControl.tsx                # React component (you create this)
├── generated/                   # Auto-generated types from manifest
├── pcf.pcfproj                  # MSBuild project file
├── package.json
└── solution/                    # Solution wrapper (created separately)
    ├── solution.cdsproj
    └── src/Other/Solution.xml
```

## Control Manifest

`ControlManifest.Input.xml` defines the control's interface:

```xml
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="MyCompany" constructor="MyControl"
           version="0.0.1" display-name-key="My Control"
           description-key="A custom control" control-type="virtual">

    <external-service-usage enabled="false">
    </external-service-usage>

    <!-- Bound property (the column value) -->
    <property name="value" display-name-key="Value"
              description-key="The bound value"
              of-type="OptionSet" usage="bound" required="true" />

    <resources>
      <code path="index.ts" order="1" />
      <platform-library name="React" version="16.14.0" />
    </resources>
  </control>
</manifest>
```

### IMPORTANT: Platform Libraries

- **React** (`version="16.14.0"`) — always include for React controls
- **Fluent** — the pac CLI scaffold includes `<platform-library name="Fluent" version="9.68.0" />` by default. **Remove it** unless you actually use Fluent UI components. Recent Fluent versions (9.68.0+) may not be supported in all Dataverse environments and cause import failures: `"platform library fluent_9_68_0 is not supported by the platform"`.
- For `control-type`: use `"virtual"` for React controls, `"standard"` for vanilla TS controls

### Property Types

| of-type | Description |
|---------|-------------|
| `SingleLine.Text` | Single line text |
| `Multiple` | Multi-line text |
| `Whole.None` | Integer |
| `Decimal` | Decimal number |
| `Currency` | Money |
| `DateAndTime.DateOnly` | Date |
| `DateAndTime.DateAndTime` | Date and time |
| `TwoOptions` | Boolean |
| `OptionSet` | Choice/picklist |
| `Lookup.Simple` | Lookup reference |

## Control Implementation

### Field Control (React)

```typescript
import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { MyComponent, IMyComponentProps } from "./MyComponent";
import * as React from "react";

export class MyControl implements ComponentFramework.ReactControl<IInputs, IOutputs> {
  private notifyOutputChanged: () => void;
  private currentValue: number | null = null;

  public init(context: ComponentFramework.Context<IInputs>,
              notifyOutputChanged: () => void): void {
    this.notifyOutputChanged = notifyOutputChanged;
  }

  public updateView(context: ComponentFramework.Context<IInputs>):
    React.ReactElement {
    this.currentValue = context.parameters.value.raw ?? null;

    // Get option set metadata (labels + values)
    const optionsMeta = context.parameters.value.attributes?.Options ?? [];
    // Each option: { Value: number, Label: string }

    const props: IMyComponentProps = {
      currentValue: this.currentValue,
      options: optionsMeta.map((opt: { Value: number; Label: string }) => ({
        value: opt.Value,
        label: opt.Label,
      })),
      onChange: (newValue: number) => {
        this.currentValue = newValue;
        this.notifyOutputChanged();
      },
      disabled: context.mode.isControlDisabled,
    };
    return React.createElement(MyComponent, props);
  }

  public getOutputs(): IOutputs {
    return { value: this.currentValue ?? undefined };
  }

  public destroy(): void { }
}
```

### ESLint Notes

The pac scaffold includes strict ESLint rules. Common fixes needed:
- Use `??` instead of ternary for nullish coalescing
- Avoid `any` types — define interfaces for option metadata
- Type `optionsMeta` explicitly to avoid `no-unsafe-*` errors

### Dataset Control

For controls bound to views/subgrids, use `dataset` template and implement `ComponentFramework.StandardControl<IInputs, IOutputs>` with `context.parameters.dataSet`.

## Development & Testing

### Test Harness

```bash
# Start local test harness (browser preview)
npm start watch
# Opens http://localhost:8181 with interactive property editors
```

### Build

```bash
npm run build
```

## Deployment

### CRITICAL: NEVER use `pac pcf push` for environments that need ALM

`pac pcf push` is a **development-only shortcut** that creates a non-exportable managed solution literally named `solution`. This solution:
- **Cannot be re-exported** from the environment (managed solutions can't be exported)
- **Cannot be transported** to other environments via solution export/import
- **Creates hidden dependencies** — if any other solution references a form with this PCF bound, that solution will fail to import in clean environments

**ALWAYS use the solution ZIP approach below for any environment where the PCF needs to be transportable.**

If `pac pcf push` was already used and the control needs to be transported:
1. Delete the managed `solution` solution from the environment (this removes the PCF)
2. Rebuild and import via the solution ZIP approach below
3. Re-bind the control to forms

### Create Solution Wrapper

```bash
# In the PCF project root, create a solution subdirectory
mkdir solution && cd solution
pac solution init --publisher-name CroweAT --publisher-prefix cr1a2

# Add reference to the parent PCF project
pac solution add-reference --path ..
```

### Build Solution ZIP

```bash
cd solution
dotnet build --configuration Release
# Output: bin/Release/solution.zip
```

### Import via MCP

```
dataverse_import_solution(
  file_base64: base64_encode("solution/bin/Release/solution.zip"),
  overwrite_customizations: true
)
```

Check import status:
```
dataverse_get_record(
  entity_set: "importjobs",
  id: "<job_id>",
  select: "importjobid,solutionname,progress,completedon"
)
```

### Add PCF Control to Your Working Solution (REQUIRED for ALM)

After importing the PCF solution, the custom control exists in the environment but is NOT in your working solution (e.g., MCPTestMatrix). If you bind it to a form in your solution, you create a cross-solution dependency that will break when deploying to other environments.

**Always add the PCF control to your working solution:**

```
# Find the custom control ID
dataverse_query_records(
  entity_set: "customcontrols",
  filter: "contains(name,'MyControl')",
  select: "name,customcontrolid"
)

# Add to your solution (component type 66 = Custom Control)
dataverse_add_solution_component(
  solution_name: "MCPTestMatrix",
  component_id: "<customcontrolid>",
  component_type: 66,
  add_required_components: true
)
```

This ensures the PCF control travels WITH your solution when exported. The `add_required_components: true` flag also pulls in the web resource dependencies (bundle.js, etc.).

## Binding PCF Control to a Form Field

After importing the solution, the control exists in Dataverse but is not yet bound to any field. Binding is done via formxml manipulation using MCP tools.

### Step 1: Verify the control name

```
dataverse_query_records(
  entity_set: "customcontrols",
  filter: "contains(name,'MyControl')",
  select: "name,customcontrolid,compatibledatatypes"
)
```

The control `name` format is: `{publisher_prefix}_{Namespace}.{ControlName}` (e.g., `cr1a2_CroweAT.TaskStatusProgress`).

### Step 2: Get the current formxml

```
dataverse_query_records(
  entity_set: "systemforms",
  filter: "formid eq '{form_id}'",
  select: "formxml"
)
```

### Step 3: Modify the formxml (two-part binding)

PCF controls are bound via a **two-part pattern** in formxml:

**Part 1** — Add a `uniqueid` attribute to the target field's `<control>` element:

```xml
<!-- BEFORE -->
<control id="cr1a2_taskstatus" classid="{3EF39988-22BB-4F0B-BBBE-64B5A3748AEE}"
         datafieldname="cr1a2_taskstatus" disabled="false" />

<!-- AFTER -->
<control id="cr1a2_taskstatus" classid="{3EF39988-22BB-4F0B-BBBE-64B5A3748AEE}"
         datafieldname="cr1a2_taskstatus" disabled="false"
         uniqueid="{GENERATED-GUID}" />
```

**Part 2** — Add a `<controlDescriptions>` section at the `<form>` level (before `<tabs>`):

```xml
<form>
  <controlDescriptions>
    <controlDescription forControl="{SAME-GUID-AS-UNIQUEID}">
      <!-- Default/fallback control (no formFactor) -->
      <customControl id="{ORIGINAL-CLASSID}">
        <parameters>
          <datafieldname>cr1a2_taskstatus</datafieldname>
        </parameters>
      </customControl>
      <!-- PCF on Web/Desktop -->
      <customControl formFactor="0" name="cr1a2_CroweAT.TaskStatusProgress">
        <parameters>
          <statusValue>cr1a2_taskstatus</statusValue>
        </parameters>
      </customControl>
      <!-- PCF on Tablet -->
      <customControl formFactor="1" name="cr1a2_CroweAT.TaskStatusProgress">
        <parameters>
          <statusValue>cr1a2_taskstatus</statusValue>
        </parameters>
      </customControl>
      <!-- PCF on Phone -->
      <customControl formFactor="2" name="cr1a2_CroweAT.TaskStatusProgress">
        <parameters>
          <statusValue>cr1a2_taskstatus</statusValue>
        </parameters>
      </customControl>
    </controlDescription>
  </controlDescriptions>
  <tabs>
    ...
  </tabs>
</form>
```

**Key details:**
- `forControl` must exactly match the `uniqueid` on the `<control>` element
- The first `<customControl>` (no `formFactor`) is the default OOB fallback — its `id` should be the original `classid` of the field control
- `formFactor`: `0` = Web/Desktop, `1` = Tablet, `2` = Phone
- `name` on each formFactor entry is the control name from the `customcontrols` entity
- Inside `<parameters>`, element names match manifest property names, content is the bound field's logical name
- For static (non-field-bound) parameters: `<myParam static="true" type="Whole.None">100</myParam>`

### Step 4: Update and publish

```
dataverse_update_record(
  entity_set: "systemforms",
  id: "{form_id}",
  data: { "formxml": "<updated formxml>" }
)

dataverse_publish_all()
```

### Common Field Control ClassIDs

| ClassID | Field Type |
|---------|-----------|
| `{4273EDBD-AC1D-40D3-9FB2-095C621B552D}` | Text (SingleLine) |
| `{3EF39988-22BB-4F0B-BBBE-64B5A3748AEE}` | OptionSet/Choice |
| `{5B773807-9FB2-42DB-97C3-7A91EFF8ADFF}` | DateTime |
| `{533B9E00-756B-4312-95A0-DC888637AC78}` | Decimal/Float |
| `{E0DECE4B-6FC8-4A8F-A065-082708572369}` | Memo (Multi-line) |
| `{B0C6723A-8503-4FD7-BB28-C8A06AC933C2}` | Boolean |
| `{270BD3DB-D9AF-4782-9025-509E298DEC0A}` | Lookup |

## Common Patterns

### Web API Calls from PCF

```typescript
const result = await context.webAPI.retrieveMultipleRecords(
  "account", "?$select=name&$top=10"
);
```

### Navigation

```typescript
context.navigation.openForm({
  entityName: "account",
  entityId: recordId,
});
```

### Utility Functions

```typescript
const userId = context.userSettings.userId;
const userName = context.userSettings.userName;
```

## Key Constraints

- **Fluent UI version** — remove the Fluent platform-library from the manifest unless needed; newer versions may not be supported in all environments
- PCF controls run in a sandboxed iframe (no direct DOM access outside the control)
- Maximum 5MB for control bundle size
- No server-side code — client-side only
- Must handle `null` values gracefully (columns can be empty)
- `updateView` is called by the platform — don't call it manually
- Use `notifyOutputChanged()` to trigger the platform to read `getOutputs()`
- `pac pcf init` requires pac CLI installed locally — `winget install Microsoft.PowerAppsCLI`

## Adding Subgrids to Forms

Subgrids showing related records can also be added via formxml. Add a new `<section>` with a subgrid `<control>`:

```xml
<section name="section_related" id="{NEW-GUID}" showlabel="true" showbar="false"
         columns="1" IsUserDefined="1" locklevel="0" visible="true"
         celllabelalignment="Left" celllabelposition="Left" labelwidth="115"
         layout="varwidth">
  <labels><label description="Related Records" languagecode="1033" /></labels>
  <rows><row>
    <cell id="{NEW-GUID}" showlabel="false" locklevel="0" visible="true"
          auto="false" rowspan="6" colspan="1">
      <labels><label description="Related" languagecode="1033" /></labels>
      <control id="subgrid_related" classid="{E7A81278-8635-4D9E-8D4D-59480B391C5B}"
               uniqueid="{NEW-GUID}">
        <parameters>
          <TargetEntityType>cr1a2_childtable</TargetEntityType>
          <EnableQuickFind>true</EnableQuickFind>
          <EnableViewPicker>false</EnableViewPicker>
          <ViewId>{VIEW-GUID}</ViewId>
          <IsUserView>false</IsUserView>
          <RelationshipName>cr1a2_parent_child_relationship</RelationshipName>
          <AutoExpand>Fixed</AutoExpand>
          <RecordsPerPage>10</RecordsPerPage>
        </parameters>
      </control>
    </cell>
  </row></rows>
</section>
```

**Subgrid ClassID:** `{E7A81278-8635-4D9E-8D4D-59480B391C5B}`

Use `dataverse_list_relationships` to find the relationship name and `dataverse_list_views` to find the view ID.

## MCP Tools Reference

| Tool | Purpose |
|------|---------|
| `dataverse_import_solution` | Import the PCF solution ZIP |
| `dataverse_query_records` | Query `customcontrols` to verify control name |
| `dataverse_update_record` | Update `systemforms` formxml to bind control |
| `dataverse_publish_all` | Publish after formxml changes |
| `dataverse_list_relationships` | Find relationship names for subgrids |
| `dataverse_list_views` | Find view IDs for subgrids |
| `dataverse_get_form` | Review current form structure |

## Validation

After deploying a PCF control:
- Verify the control appears in the target solution's components
- Open the form where the control is bound and verify it renders (consider `/validate-ui` for a visual spot-check)
- Check browser console for any control initialization errors
- Report: control name, bound field, solution it was added to, form it appears on
