Skip to content

Config-Driven PCF Components - Stop Hardcoding Table Names

How to build PCF controls that work on any Dataverse entity without rebuilding. The JSON web resource pattern with real examples from two production controls.

Victoria Pechenizka 6 min read

You build a PCF control for Account. It works perfectly. Then the admin asks: “Can we use it on Contact too?”

You know the answer. Open the source, find every hardcoded account, replace with contact, rebuild, repackage, reimport, retest. Repeat for Opportunity, Case, Lead, and whatever custom entity the client just created.

This is the wrong way to build PCF controls. There’s a better pattern, and once you learn it you’ll never hardcode a table name again.

The Pattern: JSON Web Resource as Config

Instead of baking entity and column names into your TypeScript, you externalize them into a JSON web resource that the PCF loads at runtime.

The maker provides the web resource name through a single manifest property. The PCF reads it once during init(), parses the JSON, and uses it for every API call and every UI decision.

No table names in code. No column names in code. No rebuild when you add a new entity.

How It Works

  1. 1

    Create a JSON web resource in Dataverse

    Name it following your publisher prefix (e.g. vp365_AuditHistoryConfig). Put your table names, column names, modes, and settings in a structured JSON object.

  2. 2

    Add one manifest property to your PCF

    A single SingleLine.Text input property called configWebResourceName. The maker types the web resource name when binding the control.

  3. 3

    Load and parse on init()

    During init(), query the Dataverse Web API for the web resource by name, decode the Base64 content, parse the JSON, merge with defaults.

  4. 4

    Use the config everywhere

    Every API call, every UI label, every field reference reads from the parsed config object. Nothing is hardcoded.

Here’s what the loading code looks like:

// Load config web resource by name
const result = await context.webAPI.retrieveMultipleRecords(
  'webresource',
  `?$select=content&$filter=name eq '${configName}'`
);

if (result.entities.length > 0) {
  // Content is Base64 encoded
  const decoded = atob(result.entities[0].content);
  const userConfig = JSON.parse(decoded);
  // Deep merge with defaults
  config = deepMerge(DEFAULT_CONFIG, userConfig);
}

Real Example: Field Audit History

Field Audit History uses this pattern to control which fields show audit icons on any entity. The config has four modes:

Mode What It Does When to Use
audited (default) Icons on fields where IsAuditEnabled = true Most orgs. Follow your existing Dataverse audit settings.
include Icons ONLY on listed fields Show only the 3-4 fields users care about.
exclude All audited fields EXCEPT listed ones Hide system noise like modifiedon, modifiedby.
all Icons on every visible field Compliance reviews where you need maximum visibility.

The config also controls features (restore, copy, export), display settings (panel width, date format, value preview length), pagination (page size, max pages), and every label in the UI. All from one JSON file.

// Example: show audit icons only on key Contact fields
var config = {
    tables: {
        "*": { mode: "audited", fields: [] },
        "contact": {
            mode: "include",
            fields: ["emailaddress1", "telephone1", "jobtitle"]
        },
        "account": {
            mode: "exclude",
            fields: ["modifiedon", "modifiedby"]
        }
    }
};

An admin adds a new entity? Edit the JSON, add a line. No developer needed. No solution import. Takes effect on next form load.

Real Example: Questionnaire Suite

We built a second PCF suite - a dynamic questionnaire renderer and template builder for Dynamics 365. Same pattern, much bigger config.

This PCF renders evaluation forms at runtime based on rows in Dataverse tables (sections, questions, answer options). Every table name, every column name, every relationship, every status GUID comes from the config web resource. Zero hardcoded Dataverse logical names.

The config has 27 domains and ~135 parameters. Evaluations, reviews, cycles, templates, sections, questions, answers, visibility rules, signing steps, grade expectations, competency mappings. Every one of these maps to a Dataverse table with specific column names and status GUIDs.

Why this many? Imagine delivering the same solution to multiple clients as a product. Each client has their own Dataverse schema, their own table names, their own status values, their own business rules. One client runs performance reviews with 6 signing statuses. Another uses 3. One tracks competencies through a grade hierarchy. Another doesn’t.

If you hardcode any of that, you’re maintaining a fork per client. Config-driven means one codebase, one solution ZIP, one deployment pipeline. The client’s admin edits a JSON file to map the PCF to their schema. Different tables, different statuses, different roles - same control.

And performance matters here. The PCF loads the config once during init(), parses it, caches the object in memory. Every subsequent API call and UI render reads from that cached object. No repeated web resource fetches, no runtime parsing. One load, done.

The Config Interface

Type your config. This is the most important part. Don’t use any or untyped JSON parsing.

export interface ITableConfig {
    mode: "audited" | "include" | "exclude" | "all";
    fields: string[];
}

export interface IConfig {
    _version: string;

    features: {
        allowRestore: boolean;
        allowCopy: boolean;
        allowExport: boolean;
    };

    tables: Record<string, ITableConfig>;

    labels: {
        panelTitle: string;
        noRecordsMessage: string;
        // ... every UI string
    };
}

Every property is typed. Every property has a default. The PCF knows at compile time exactly what shape the config has. No runtime surprises.

What Admins Can Do Without a Developer

This is the real selling point. Once the PCF is deployed with this pattern:

  • Add a new entity - edit the JSON, add a table entry
  • Hide noisy fields - add field names to the exclude list
  • Change labels - update any string in the config
  • Toggle features - set allowRestore to false for read-only environments
  • Environment-specific settings - dev has "all" mode for testing, prod has "audited" mode

All done in the Dataverse web resource editor. No VS Code, no npm, no solution import. Changes take effect on next form load.

Implementation Checklist

If you’re building a config-driven PCF, get these right:

  • One manifest property - configWebResourceName (SingleLine.Text, input, optional)
  • Typed interfaces - TypeScript interfaces for every config section
  • DEFAULT_CONFIG constant - every property set, used as fallback
  • Deep merge - user config overrides defaults, missing properties use defaults
  • Graceful failure - if web resource is missing, control works with defaults and logs a warning
  • Schema validation - validate the parsed JSON against your interface at runtime
  • Base64 decoding - Dataverse stores web resource content as Base64
  • Cache the config - parse once during init(), reuse for the component’s lifetime
  • No hardcoded logical names - if you find a table or column name as a string literal in your code, move it to config
  • Unit test the config loading - mock the web resource response, test defaults, test partial overrides, test missing resource

The Alternative: 29 Manifest Properties

We tried the other approach first. One manifest property per setting. The form designer UI becomes a wall of text inputs. Makers can’t see what they’re configuring. Adding a new setting means bumping the control version and reimporting the solution.

The JSON web resource pattern reduces this to a single property. The config file is readable, diffable, and version-controllable.

Start Here

If you’re building a PCF that touches more than one entity, make it config-driven from day one. The upfront cost is one interface file and one loader function. The payoff is never rebuilding your control because someone wants it on a different table.

See Field Audit History for a complete working example - fully typed config interfaces, sensible defaults, four modes, and zero hardcoded table names. The actual config a maker edits is about 15 lines of JavaScript.


VP365.ai - Power Platform tools for practitioners who ship. Follow vp365.ai on LinkedIn for new controls and deep dives.

Stay in the loop

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

Related articles