Custom PCF Controls Development
Purpose-built PowerApps Component Framework controls in TypeScript and React - for model-driven apps, canvas apps, and Power Pages.
Solzet is a specialized PCF (PowerApps Component Framework) controls development team. We build custom UI components in TypeScript and React that extend Dynamics 365 and Power Apps beyond what out-of-the-box controls offer - interactive data grids, map visualizations, signature capture, rich text editors, drag-and-drop interfaces, and domain-specific widgets. Our developers have deep experience with the PCF lifecycle (init, updateView, getOutputs, destroy), Dataverse Web API integration, and responsive design for both model-driven and canvas app hosting. Based in Yerevan, Armenia, we deliver PCF controls remotely to Microsoft partners and end clients across Europe and the US on B2B contracts. This page also sets out our full PCF development lifecycle in detail, covering planning and the manifest contract, coding against the component lifecycle, testing in the harness and in a real environment, and a deployment checklist for versioning and promoting the managed solution. The development guide below adds the two questions a lifecycle assumes you have already answered: whether the control should be configured away, adopted, customized, built in house or commissioned at all, and how a third party charting, mapping or scheduling library should reach the browser, followed by a worked log of one control from the first scoping call to the production import.
What is a PCF control and when do you need one?
A PCF (PowerApps Component Framework) control is a custom UI component written in TypeScript (optionally with React or other frameworks) that replaces or enhances a standard field, dataset, or section in a Dynamics 365 or Power Apps form. You need a PCF control when the out-of-the-box controls cannot deliver the user experience your business requires - for example, an interactive map showing customer locations pulled from a Dataverse table, a Gantt chart for project scheduling, a barcode scanner for warehouse operations, a rich signature capture pad for field service, or a custom data grid with inline editing, conditional formatting, and export capabilities. PCF controls are the Microsoft-recommended successor to legacy HTML web resources, offering better performance, lifecycle management, and cross-device compatibility.
How does Solzet build PCF controls?
Our PCF development process starts with a detailed specification: what data the control reads and writes, which Dynamics 365 form or canvas app it lives in, and the exact UX requirements including responsive behavior. We then build the control using TypeScript and React (our standard choice for complex UIs), leveraging the Fluent UI component library for visual consistency with the Dynamics 365 interface. Development follows Microsoft best practices: proper use of the ComponentFramework.Context API, efficient rendering with updateView, cleanup in destroy, and manifest configuration for bound/input/output properties. We test across browsers (Edge, Chrome, Firefox) and devices (desktop, tablet, mobile). Delivery includes the managed solution (.zip) ready for import into your Dataverse environment, plus source code and documentation so your team can maintain it.
PCF controls development for Dynamics 365: the development guide
This is the guide the rest of the page runs on, written for whoever has to deliver a code component rather than evaluate one. It answers three questions in order. Should this control exist at all. What actually happens in each of the four phases of PCF controls development. And what the whole thing looks like end to end, from the first scoping call to the production import, on a real Dynamics 365 project rather than on a clean tutorial environment.
Start with the decision, because a meaningful share of the PCF requests we are asked to quote should never become a PCF project. The tree below is the version we run in a scoping call and it takes about twenty minutes. Five questions, five verdicts, and only one of those verdicts is a new control. Reaching the wrong verdict is expensive in a specific way: a component nobody needed still has to be versioned, tested against two release waves a year, and owned by somebody after go live.
Two of the verdicts have a full guide of their own on this site, because they deserve more than a branch on a tree. PCF controls: buy, build, or customize costs the three routes side by side, including the year two number most teams work out too late, and gives worked verdicts for schedulers, map pickers, and large dataset viewers. Our own PCF controls library is where the adopt branch starts, since a maintained component that already does the job is the cheapest correct answer on this page.
- Configure. A supported first party feature covers most of it: the modern grid, the editable grid, a first party map control, a business rule, a better view, or an embedded canvas app. Take the eighty percent and stop. No manifest, no version, no owner.
- Adopt. The behaviour is generic and a maintained component already does it. Import it, configure it on the form, and name somebody to own it at the next release wave. Judge the component by when it was last updated rather than by how popular it looks.
- Customize. A component is close but not exact. Worth it only when the licence allows a fork and the delta is genuinely small, because from the moment you edit it you own the merge for every future update. If the delta is not small, price it as a build.
- Build in house. The behaviour is yours, the form is not business critical, and you can spare a developer for the learning curve as well as for the code. The four phases below are written so that you can run them without us.
- Commission. The behaviour is yours and the blast radius is large: every seller or every service agent touches it, or it writes to data that invoicing depends on. A written specification and a fixed price move the risk of a misunderstanding onto the supplier, which is the actual thing being bought.
A business request for something the form cannot do today
|
1. Does a supported first party feature already cover most of it?
|-- yes -> CONFIGURE. Modern or editable grid, first party map,
| business rule, view, embedded canvas app. Stop here.
'-- no -> question 2
|
2. Is the behaviour generic, or does it encode your own process?
|-- generic -> question 3
'-- yours -> question 5
|
3. Does a maintained component already do it?
(our library and the community galleries; read the last commit
date, not the star count)
|-- yes -> question 4
'-- no -> question 5
|
4. Does it fit with configuration alone, no behaviour changes?
|-- yes -> ADOPT. Import, configure, name a release wave owner.
'-- no -> CUSTOMIZE when the licence allows a fork and the
delta is small. Otherwise price the fork as a build
and go to question 5.
|
5. How many people touch it, and what does it write to?
|-- few users, low blast radius -> BUILD IN HOUSE, if you can
| spare the learning curve.
'-- many users, invoiced data,
business critical form -> COMMISSION against a signed
specification at a fixed
price. Phase 1 starts here.The PCF control development lifecycle: planning, coding, testing, deployment
The official framework documentation shows you how to make a control run on your own machine. It stops there. It does not tell you how to specify a control so the estimate survives contact with the business, how to keep updateView cheap when the form it sits on already carries a dozen other customisations, how to test something the local harness cannot reproduce, or how to get the result through a change board and into production with a version number you can roll back to. That gap is where PCF projects actually fail, so this is our full lifecycle, written out so you can run it in house or hold our delivery to it.
Four phases, each with a deliverable rather than a feeling of progress. Planning produces a signed specification and a manifest. Coding produces a control that respects the component lifecycle instead of fighting it. Testing produces evidence from a real environment, not only from the harness. Deployment produces a versioned managed solution and a repeatable path from development to production. The most expensive mistake we are called in to fix is a project that started at phase two, because a control written before anyone agreed what it reads, what it writes, and where it lives becomes a rewrite rather than a change request.
The decision tree in the guide above settles whether the control should exist, so everything from here assumes the verdict was build or commission. The one question left before phase one is who owns each phase, and the team against freelancer comparison works that through: what a PCF engagement contains beyond TypeScript and React, and the screening questions that tell a developer who has shipped controls into production from one who has read about them.
- Phase 1, planning. Deliverable: a written specification, an agreed ControlManifest.Input.xml, and a decision on host, control type, and data access. Typically days, not weeks, and it is the phase that fixes the price.
- Phase 2, coding. Deliverable: a control that does expensive work once in init, cheap work in updateView, hands values back through getOutputs, and releases everything in destroy.
- Phase 3, testing. Deliverable: harness runs for the fast loop, plus evidence from a real environment under a real security role, with a real dataset, on the devices your users actually hold.
- Phase 4, deployment. Deliverable: a versioned managed solution, an import that is repeatable in test and production, a rollback position, and a named owner for the next two release waves.
Phase 1: planning a PCF control before any code is written
Planning a code component is not a design exercise, it is a contract exercise. Four decisions carry almost all of the cost, and all four are cheap to change now and expensive to change once there is a codebase. First, the host: model-driven app form, editable grid, custom page, canvas app, or Power Pages, because that decides which platform APIs exist for you. Second, the control type: a field control bound to one column, or a dataset control bound to a view, which are different components with different manifests rather than two settings of one thing. Third, standard or virtual: a virtual control borrows React and Fluent from the platform, so the bundle is smaller and the theming matches the host, while a standard control carries its own rendering stack and is the safer choice when you need a library the platform does not provide. Fourth, data access: everything through bound properties and the dataset, or a declared Web API feature for reads the binding cannot reach.
Then write down the behaviour in the language of the people who asked for it, not in React terms. What happens on an empty record, on a read only form, for a user whose security role hides one of the columns, at ten rows and at ten thousand, and offline on a phone. Every answer you do not write down becomes a change request later, and change requests are where fixed price projects turn into time and materials.
The manifest is where those decisions become binding, so we agree it before development starts. Anything you might want to configure per environment or per form belongs there as an input property, because a value in the manifest is changed by an administrator in seconds while a value in the code is a rebuild, a version bump, and a solution import. Declare feature usage explicitly as well: context.webAPI, device access, and the utility APIs simply do not exist at runtime unless the manifest asks for them, and the failure mode is an undefined object in production rather than a build error in development.
- Data in and data out. Every bound, input, and output property with its exact type, and what the control does when a value is null or the user has no read access to it.
- Host and form context. Which app, which form or view, whether the same control must also run in a canvas app, and what it does when the form is read only or the record is inactive.
- Volume and performance budget. Expected and worst case row counts, page size, and the form load time the control must not exceed. Write the number down, then test against it in phase three.
- Security expectations. Which roles see what, whether field level security applies to any bound column, and whether the control must degrade gracefully rather than error when a read is denied.
- Configurability. Anything you would otherwise hard code: colours, thresholds, labels, target tables, page size. Each one becomes an input property in the manifest.
- Accessibility and localisation. Keyboard operation, screen reader labels, high contrast, right to left, and which languages need resx string files.
- The deliverable itself. Source repository, managed and unmanaged solution artifacts, the publisher prefix, documentation, and who owns the code and the upgrade after go live.
- Definition of done. The specific scenarios that will be demonstrated at sign off, agreed now rather than negotiated at the end.
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="Solzet" constructor="OrderGrid" version="1.0.0"
display-name-key="OrderGrid" description-key="Editable order grid"
control-type="virtual">
<!-- Dataset control: bound to a view, not to a single column. -->
<data-set name="records" display-name-key="Orders"
cds-data-set-options="displayCommandBar:true;displayQuickFind:true">
<property-set name="amount" display-name-key="Amount"
of-type="Currency" usage="bound" required="true" />
</data-set>
<!-- Configurable by an administrator per form. No rebuild required. -->
<property name="pageSize" display-name-key="Page size"
of-type="Whole.None" usage="input" required="false" />
<property name="highlightThreshold" display-name-key="Highlight above"
of-type="Decimal" usage="input" required="false" />
<!-- What the control hands back to the platform. -->
<property name="selectedIds" display-name-key="Selected ids"
of-type="SingleLine.Text" usage="output" required="false" />
<!-- Without this, context.webAPI is undefined at runtime. -->
<feature-usage>
<uses-feature name="WebAPI" required="true" />
</feature-usage>
<resources>
<code path="index.ts" order="1" />
<platform-library name="React" version="16.8.6" />
<platform-library name="Fluent" version="9.46.2" />
<resx path="strings/OrderGrid.1033.resx" version="1.0.0" />
</resources>
</control>
</manifest>Phase 2: coding the control against the component lifecycle
The framework gives you four methods and almost every performance problem we are asked to fix comes from putting work in the wrong one. init runs once, so it owns the expensive things: metadata reads, option set label lookups, security role checks, third party library initialisation, event subscriptions. updateView runs constantly, on every dependent property change, every dataset page, every container resize, and sometimes for reasons that have nothing to do with your control, so it owns cheap, idempotent rendering and nothing else. getOutputs runs when the platform asks for values after you called notifyOutputChanged, and it must be a pure read of state rather than a place where work happens. destroy runs when the control leaves the DOM, and skipping it is how a user who scrolls a subgrid for ten minutes ends up with a leaking browser tab.
Two habits separate a control that survives its second year from one that gets rewritten. Use context.updatedProperties to find out what actually changed instead of rebuilding everything, and create your rendering surface once and update it in place rather than tearing it down. Where a third party library is involved, both habits become mandatory, because charting and mapping libraries attach their own listeners and SVG nodes. Our walkthrough of integrating third party libraries such as Highcharts into a PCF control covers that pattern in full, including webpack externals against bundling and the disposal code that prevents the leak.
Dataset controls need one more discipline. The platform hands you a page, never the whole result set, so read paging.totalResultCount before you write any logic that assumes the grid holds everything, and call loadNextPage deliberately in response to a user action rather than in a loop that walks the entire table on form load. On the write side, prefer bound and output properties over direct Web API writes, because bound values go through the platform save pipeline where business rules, plugins, and audit already work.
- Expensive once, cheap often. Metadata, labels, role checks, and library setup in init. Rendering in updateView. Nothing surprising in getOutputs.
- Render once, update in place. Keep the instance, apply new data to it, and never rebuild the DOM tree on every updateView. This is the single largest cause of flicker and of leaked nodes.
- Release everything in destroy. Event listeners, timers, observers, subscriptions, and any third party instance you created. Assume the control will be mounted and unmounted hundreds of times per session.
- Guard the optional platform. context.webAPI exists only when the manifest declared it and the host provides it, so a control that must also run in a canvas app has to fail soft rather than throw.
- Respect the container. Call context.mode.trackContainerResize(true) and lay out against allocatedWidth and allocatedHeight rather than against window dimensions, or the control will be wrong inside a subgrid and wrong again on a phone.
- Respect read only and disabled states. Check context.mode.isControlDisabled and the field level security flags on bound properties before rendering anything editable.
- Type against the generated types. Run refreshTypes after every manifest change so ManifestTypes.d.ts matches the contract, and the compiler catches a renamed property instead of production catching it.
- Keep the bundle honest. A virtual control using platform React and Fluent, plus tree shaking and an externalised heavy library, is the difference between a form that loads and a form that people complain about.
export class OrderGrid
implements ComponentFramework.ReactControl<IInputs, IOutputs> {
private notifyOutputChanged: () => void;
private selectedIds: string[] = [];
private currencyFormat: string;
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
): void {
this.notifyOutputChanged = notifyOutputChanged;
// Once only. Metadata reads, label lookups and library setup belong here,
// never in updateView, which the platform may call hundreds of times.
this.currencyFormat = context.userSettings.numberFormattingInfo.currencySymbol;
context.mode.trackContainerResize(true);
}
public updateView(
context: ComponentFramework.Context<IInputs>,
): React.ReactElement {
const records = context.parameters.records;
// Do not render a half loaded dataset, and do not rebuild the tree when
// only the container size moved.
if (records.loading) {
return React.createElement(GridSkeleton);
}
return React.createElement(Grid, {
records,
readOnly: context.mode.isControlDisabled,
width: context.mode.allocatedWidth,
threshold: context.parameters.highlightThreshold.raw ?? 0,
currencySymbol: this.currencyFormat,
onSelect: (ids: string[]) => {
this.selectedIds = ids;
this.notifyOutputChanged();
},
});
}
public getOutputs(): IOutputs {
// A pure read of state. No work, no side effects, no async.
return { selectedIds: this.selectedIds.join(',') };
}
public destroy(): void {
// Every listener, timer and third party instance created above is released
// here. Skipping this is how a long session leaks a tab.
}
}// The platform hands you one page, never the whole table. Read the total
// before writing anything that assumes the grid holds every row.
const ds = context.parameters.records;
const pageSize = context.parameters.pageSize.raw ?? 100;
if (ds.paging.pageSize !== pageSize) {
ds.paging.setPageSize(pageSize);
}
// Load on a user action. Never loop loadNextPage on form load: a 40,000 row
// table will do exactly what you told it to.
if (ds.paging.hasNextPage && this.userRequestedMore) {
this.userRequestedMore = false;
ds.paging.loadNextPage();
}
// context.webAPI is undefined unless the manifest declared the feature and the
// host supports it. Canvas hosting does not, so fail soft rather than throw.
const canQuery = typeof context.webAPI?.retrieveMultipleRecords === 'function';
const related = canQuery
? await context.webAPI.retrieveMultipleRecords(
'account',
'?$select=name,revenue&$filter=statecode eq 0&$top=50',
)
: { entities: [] };Phase 3: testing a PCF control before it goes anywhere near production
Testing happens in two places and teams that only use the first one ship the bugs we get paid to find. The local harness, started with npm start watch, is the fast loop: sample data, a resize slider, a container size control, and a reload on every save. It is excellent for layout, formatting, and interaction work. What it cannot do is the part that breaks in production. There is no real Dataverse behind it, so context.webAPI is not available, security roles do not apply, field level security does not apply, paging behaves nothing like a real view, and the form context around your control does not exist.
So the second place is a real development environment, with the control on a real form, holding real volumes, opened by a user who is not a system administrator. That last point catches more defects than any other single test. A control built and demonstrated by an administrator will happily hide a query the business user is not allowed to run, and it will fail on the first day of user acceptance testing rather than in your sprint. Create a test user with the actual production role, remove your own privileges for an hour, and open the form.
Alongside that, keep pure logic out of the framework so it can be unit tested normally. Sorting, grouping, threshold evaluation, currency and date formatting, and validation rules should live in plain functions that a test runner can call without a host, which is also what makes them cheap to reverify after each release wave. Two release waves a year plus your own React and component library upgrades mean a control is never finished being tested, only tested most recently.
- Harness first, environment second. Layout, resize, and interaction in npm start watch. Everything involving data, permissions, or volume in a real environment.
- Test as a non-administrator. A test user with the production security role, and where relevant a role with field level security restrictions on a bound column.
- Test at real volume. The worst case row count from the specification, not twenty sample rows, with paging exercised and the form load budget measured in browser developer tools.
- Test the empty and broken states. New record with no data, deleted related record, null in a required bound column, and a denied read. None of these should produce a blank control or a stack trace.
- Test the hosts you promised. Unified interface on desktop, the mobile app on a real phone, and if the specification said so, a canvas app, where the Web API is unavailable and data arrives through Power Fx.
- Test read only and disabled forms. Business process locked records, inactive records, and forms where the control is disabled by a business rule.
- Test accessibility. Keyboard only operation from tab in to tab out, screen reader labels on interactive elements, and high contrast mode.
- Unit test the logic, not the framework. Plain functions for calculation and formatting, with a test runner, so the release wave regression check takes minutes instead of a day.
npm run refreshTypes # regenerate ManifestTypes.d.ts after any manifest change
npm run build # type check and bundle, treat warnings as failures
npm start watch # local harness: layout, resize and interaction only
npx jest --coverage # the logic that does not need a host to be correct
# Reality check the bundle before anyone sees the form.
# A heavy control on a busy form is a support ticket waiting to happen.
ls -lh out/controls/OrderGrid/bundle.jsPhase 4: deployment, versioning, and the release checklist
A control is deployed as part of a solution, and the rules that govern solutions are what most tutorials skip. Development environments get unmanaged, everything downstream gets managed, and the artifacts for both come from the same build so what you tested is what you import. pac pcf push is a development convenience that puts an unmanaged control straight into your development environment: it is genuinely useful in the inner loop and it should never be how anything reaches test or production, because it leaves you with no versioned artifact to roll back to.
Version discipline is the other half. Increment the version in ControlManifest.Input.xml on every release and increment the solution version with it. If you do not, the platform can serve a cached copy of the previous bundle and you will spend an afternoon debugging code that is not running. After any import, publish all customizations, then hard refresh, then verify against the actual form rather than against the solution import log. Where two solutions both contain the control, remember that the last managed import wins in the layer, which is why a shared publisher prefix and a single owning solution matter more than they sound.
Finally, decide who owns the control after go live, and write it into the statement of work rather than leaving it implied. Two release waves a year, plus dependency updates in React and the component library you chose, mean a control drifts quietly until a change request or a browser update exposes it. That ownership question is exactly the one covered in our comparison of hiring a PCF team against a freelancer, and it is the difference between a component you maintain and a component you eventually replace.
- Version bumped. ControlManifest.Input.xml and the solution version both incremented, and the change recorded against a work item.
- Built clean. A fresh build from the committed source with no local uncommitted changes, producing managed and unmanaged artifacts from the same run.
- Publisher and prefix correct. The same publisher across environments, because changing it later means a new component rather than an update.
- Dependencies present. Every table, column, view, choice, and security role the control assumes exists in the target environment, or is in the same solution.
- Import path proven. The managed solution imported into a test environment that resembles production, not only into the environment it was built in.
- Customizations published and verified. Publish all customizations, hard refresh, then open the real form as a real user and confirm the new behaviour is actually running.
- Rollback position known. The previous managed solution artifact kept and importable, and the team agreed in advance on what triggers a rollback.
- Performance re-measured in production. Form load timed on the production form, not on the developer machine, because production has more customisations, more data, and slower networks.
- Handover complete. Source in your repository, manifest decisions documented, configuration properties explained to whoever administers the app, and IP ownership stated in writing.
- Owner named for the next release wave. A person or a supplier with a support arrangement, agreed before go live rather than looked for afterwards.
# Once, in a folder alongside the control project.
pac solution init --publisher-name solzet --publisher-prefix slz
pac solution add-reference --path ../OrderGrid
# In the .cdsproj, so one build produces both artifacts:
# <SolutionPackageType>Both</SolutionPackageType>
# Every release. Bump the manifest version first, then build.
dotnet build --configuration Release
# Development only. Fast inner loop, unmanaged, never a promotion path.
pac pcf push --publisher-prefix slz
# Promote the managed artifact through test, then production.
pac auth create --environment https://contoso-test.crm4.dynamics.com
pac solution import --path bin/Release/Solution.zip --publish-changes --async
# Then verify on the form as a non-administrator, not in the import log.The deployment failure we are called about most
The control works in development, the managed solution imports into production without an error, and the form shows the old behaviour. Nine times out of ten the manifest version was not incremented, so the platform kept serving the cached bundle. The remaining cases are usually a missing dependency the development environment happened to have, a feature declared in one solution but imported from another, or customizations that were never published. All four are checklist failures rather than code failures, which is why the checklist above exists and why we run it on every release rather than on the ones that feel risky.
Third party libraries, and a worked control from scoping to production
Most controls worth commissioning need something the platform does not ship: a charting engine, a mapping SDK, a timeline or scheduler renderer, a rich text editor, a document viewer. How that library reaches the browser is decided in phase one and paid for in phase four, so it belongs in the specification next to the manifest rather than in a pull request three weeks later. There are four patterns, and the choice between them is usually made by the environment rather than by taste.
Whichever pattern you pick, the integration rules are the ones the component lifecycle already implies: create the instance once, update it in place, and release it in destroy. A charting or mapping library attaches listeners and nodes of its own, so a control that rebuilds the instance on every updateView will flicker on a busy form and leak a tab over a long session. The full code for that pattern, with Highcharts as the worked example, is in our guide to integrating third party libraries into a PCF control, including the webpack externals configuration and the disposal code, so it is not repeated here.
The log below puts the four phases and the library decision together on a single control. It is a composite of engagements rather than one named client, with the days rounded, and it is the same shape as the eight controls two of our developers built white label for a UK Microsoft Solutions Partner in the PCF controls development case study. Read it as the answer to how long a Dynamics 365 control takes and where the time actually goes, which is rarely where people expect.
- Platform library. React and Fluent declared in the manifest and borrowed from the host. Smallest bundle, theming that follows the environment automatically, and the default for a virtual control on a model-driven form.
- Bundle it with npm. Install, import, and let the build pipeline place the library inside bundle.js. Self contained, works where outbound script loads are blocked, and the right default for a small library or a locked down tenant. The cost is bundle size on every form load.
- Externalise it and load from a content delivery network. The import resolves to a global at runtime and several controls share one cached copy. Cheaper on a form carrying multiple controls, and it adds two failure modes: the network has to be reachable from managed devices, and the environment content security policy has to allow it.
- Keep it off the form entirely. Some libraries do not belong in a code component: heavy rendering, anything needing a server side licence key, or a document engine that is happier as a service. Do the work in an Azure function and let the control display the result.
- Settle licensing before any code. Commercial charting, scheduling, and grid libraries are licensed per developer, per application, or per domain. That question is cheap in the specification and expensive once a control has been written against the library.
Day 1 Scoping call. The ask: a drag and drop scheduling grid
on the work order form. Run the decision tree first. The
first party schedule board is ruled out because bookings
live in a custom table the client already reports on.
Verdict: commission.
Day 2-3 Specification. Host: model-driven form. Type: dataset
control, virtual, platform React and Fluent. In: six
bound columns. Out: selected ids plus a moved booking
payload the form saves. Volume: 400 bookings a week,
worst case 2,000 rows in view. Budget: form load under
two seconds with the control on it. Roles: dispatchers
move bookings, technicians are read only.
Day 3 ControlManifest.Input.xml agreed and signed with the
specification. pageSize, slotMinutes and the colour
thresholds become input properties, because those are
the three things clients change twice during user
testing. Fixed price set. Everything after this is a
change request rather than an argument.
Day 4 Library decision. The timeline renderer is not something
the platform ships. Bundled with npm rather than loaded
from a network, because the client blocks outbound
script loads on managed devices. Licence purchased
before the first commit.
Day 5-12 Build. init reads user settings, time zone and choice
labels once. updateView renders, then updates the same
instance in place. getOutputs returns the moved booking.
destroy disposes the renderer and removes the resize and
drag listeners.
Day 13 Harness pass. Layout, resize, three container widths,
empty state, single row, and a null in a bound column.
Day 14 Real environment. 2,000 rows imported, paging exercised
deliberately, form load measured at 1.4 seconds in
browser developer tools against the two second budget.
Day 15 Opened as a technician test user rather than as an
administrator. Two defects the harness cannot produce: a
denied read on a cost column threw, and a read only form
still allowed a drag. Both fixed the same day.
Day 16 Accessibility pass, keyboard only from tab in to tab
out, then the mobile app on a real phone.
Day 17 Version incremented in the manifest and in the solution.
Managed and unmanaged artifacts built from committed
source in one run.
Day 18 Import into a test environment, publish all
customizations, hard refresh, verify on the real form as
a real user. Previous managed artifact kept for
rollback.
Day 19 Production import in the next release window. Form load
re-measured in production. Handover pack delivered and
an owner named for the next release wave.Where the time actually goes
Nineteen working days in that log, and eight of them are the build. The rest is the specification that fixes the price, the testing a local harness cannot do, and the deployment discipline that keeps a rollback available. Teams that estimate a control by its build alone are quoting less than half of the work, which is the usual reason a fixed price on PCF development stops being fixed.
What lands at the end is the same on every engagement: managed and unmanaged solutions built from committed source, the full repository, the manifest decisions documented so an administrator knows which behaviour is configurable without a developer, and IP ownership stated in the statement of work. If you already have a specification, send it and we will mark up the manifest and the phases that carry the risk before quoting. If you do not have one, the decision tree above is the first conversation.
How do you scope and price a custom PCF control?
PCF control effort varies significantly by complexity. A simple field-level control (formatted display, custom input mask) is a quick engagement. A medium-complexity control (interactive chart, filtered lookup with custom UI) takes more time. A complex dataset control (full custom grid, map visualization, drag-and-drop scheduler) is a larger project. All engagements include specification, development, testing, managed solution packaging, and documentation. Solzet offers volume pricing for Microsoft partners who need multiple controls built across client projects. Pricing is always transparent and fixed-price after specification approval - contact us for a detailed quote.
Why Companies Choose Solzet for PCF Development
If you are working through a list of Power Apps PCF development companies, the question that decides the shortlist is not who can write TypeScript and React. It is who is still responsible for the control eighteen months from now. Most of what a search returns is one of two things: a GitHub repository of open source components published by a community author, or an individual freelance developer on a marketplace. Both are legitimate, and where the component you need already exists and is actively maintained, taking it is the cheapest correct answer. Solzet is the third option on that list, and a different kind of supplier: a specialist Dynamics 365 Customer Engagement and Power Platform consultancy where PCF development is a contracted service with a written specification, a fixed price after approval, a named team, and an owner after go live.
Four things separate us from the rest of the list. Senior only delivery: every control is built by a senior engineer who has shipped controls into production, not by a junior learning the component lifecycle on your form, and our team holds Microsoft certifications PL-200, PL-400, and PL-600 alongside the Dynamics 365 Customer Engagement certifications MB-210, MB-230, and MB-240. White label subcontracting: Microsoft partners and systems integrators hire us under their own brand, using their email, their Azure DevOps or GitHub, and their communication channels with the end client, which is how a partner adds TypeScript and React capacity for one programme without hiring for it permanently. Rescue and takeover: we routinely adopt controls somebody else started, abandoned, or left undocumented, beginning with an audit of the manifest, the lifecycle, and the solution packaging before any new code is written, the narrow form of our Dynamics 365 project rescue and takeover service. Cost base: we deliver from a single hub in Yerevan, Armenia, so certified senior engineers cost less than the Western European consultancy equivalent, and GMT+4 gives 4 to 6 hours of daily overlap with Western European hours and 6 to 8 with the UK.
We also publish our own components rather than only building them to order, and our PCF controls library is the fastest way to judge the work before you commission anything, since the free controls in it are built by the same engineers to the same standard as commissioned ones. The practical difference between the three routes on your list comes down to six factors, and it is worth being explicit about them because they are the ones a repository README and a marketplace profile do not answer.
- Support after go live. Specialist consultancy: support and enhancement are part of the engagement, done by the engineers who wrote the control, with a monthly allocation or ad hoc hours on a B2B contract. Open source repository: an issues tab, answered when the author has time, and no commitment that it will be answered at all. Marketplace freelancer: available while the contract runs, and a message into the void once it ends.
- IP ownership and what you actually receive. Specialist consultancy: a managed solution ready to import, the full source in your repository, the manifest choices explained, and IP ownership defined in the statement of work, so the control is yours. Open source repository: whatever the licence permits, which is usually generous but binds you to the author choices and gives you no warranty. Marketplace freelancer: negotiable, and frequently the gap in the contract, which is how buyers end up with a working control they cannot rebuild.
- Project reliability and continuity. Specialist consultancy: a team rather than a person, so illness, holiday, or somebody leaving is a scheduling problem instead of a stalled project. Open source repository: no delivery commitment exists, because nothing was promised to you. Marketplace freelancer: one person, one calendar, and one point of failure.
- Quality, and who does the screening. Specialist consultancy: the screening happened before the engineer joined us, and PCF is what the firm does every week rather than an occasional job. Open source repository: quality varies from excellent to abandoned, and reading the code is your job. Marketplace freelancer: a strong React profile does not prove anyone has packaged a managed solution or honoured dataset paging, so the screening is yours to run.
- Release waves and year two. Specialist consultancy: two Microsoft release waves a year are tested against your controls before they land, and somebody owns the upgrade. Open source repository: the control drifts on the author schedule, and a component untouched for two waves is the real warning sign rather than a low star count. Marketplace freelancer: nobody owns the upgrade unless you re-engage the same person, if they are still reachable.
- Cost and the commercial shape. Specialist consultancy: fixed price per control after the specification is approved, volume pricing for partners commissioning several across client projects, and no recruitment fee, payroll, or notice period. Open source repository: free to acquire, and priced instead in the engineering time it takes to review, adapt, and maintain it. Marketplace freelancer: the lowest headline rate of the three, with change requests and rework as the variable nobody quotes for.
What this looks like in practice
A UK Microsoft Solutions Partner won a Dynamics 365 programme that required eight custom PCF controls and had no in-house TypeScript and React capacity for them. Two of our senior developers joined their delivery team white label for three months, working in the partner Azure DevOps and appearing to the end client as the partner. The full delivery, including what was built and what was handed over, is written up in the PCF controls development for a UK Microsoft partner case study.
If you are still deciding between supplier types rather than between suppliers, our guide to hiring PCF developers as a team or as a freelancer works through the same decision in more depth, including where to find open source controls, when a free component or a freelancer is genuinely the right call, and the screening questions to ask whoever you hire.
Can Solzet build PCF controls for canvas apps?
Yes. PCF code components are supported in canvas apps as well as model-driven apps, and we build controls that run in both. The same TypeScript and React codebase serves both hosts. What changes is how data reaches the control: a canvas app has no Dataverse column to bind to, so the app supplies values through input properties set with Power Fx, or a dataset bound to the Items property, and the control hands results back through output properties that raise OnChange. Two things catch teams out, and neither means canvas is unsupported: the component framework has to be switched on per environment before code components appear in Power Apps Studio, and the Dataverse Web API is not available to a control running in a canvas app, so data access belongs on the app side. This is particularly powerful for organizations that use canvas apps for mobile scenarios but need richer UI than standard canvas controls provide - things like camera integration, offline-capable data entry, or custom calendar views.
In-depth guides
PCF controls: buy, build, or customize?
Before commissioning anything, work out which of the three routes your control belongs in. The guide gives the five questions that decide it, the three routes costed side by side including what each one costs after year one, and worked verdicts for schedulers, map pickers, and large dataset viewers.
Integrating third party libraries into a PCF control
The coding phase in full detail, using Highcharts as the worked example. Bundling against webpack externals, loading a library from a CDN, creating the instance once and updating it in place, and the disposal code in destroy that stops a busy form leaking a chart on every record you open.
Hiring PCF developers: a team against a freelancer
Once building is ruled out, this decides supplier type. What a PCF engagement contains beyond React and TypeScript, where each buying route holds up or falls over, and the screening questions that separate a developer who has shipped controls into production from one who has read about them.
Frequently Asked Questions
What is the PowerApps Component Framework (PCF)?
The PowerApps Component Framework (PCF) is Microsoft's official framework for building custom code components that run inside Power Apps (both model-driven and canvas) and Dynamics 365 forms. PCF controls are written in TypeScript, can use React or other JavaScript frameworks, and follow a defined lifecycle (init → updateView → getOutputs → destroy). They replace the older approach of HTML web resources and offer better performance, mobile compatibility, and integration with the Power Platform ecosystem.
Should I use PCF controls or HTML web resources?
PCF controls are the Microsoft-recommended approach going forward. HTML web resources are legacy and come with limitations: they run in iframes (poor mobile experience, security restrictions), cannot access the ComponentFramework API, and do not work in canvas apps. PCF controls render natively in the form, have full API access, support responsive design, and are distributable as managed solutions. If you have existing web resources, Solzet can help migrate them to PCF controls.
Can I hire Solzet to build PCF controls as a white-label subcontractor?
Yes. Many Microsoft partners hire Solzet to build PCF controls under their brand for their end clients. We work under NDA, use your project tooling and communication channels, and deliver controls that your team packages and deploys. This is ideal for partners who need TypeScript/React expertise for a specific project without hiring a permanent developer.
What technologies does Solzet use for PCF development?
Our standard PCF stack is TypeScript + React + Fluent UI, built with the PAC CLI (Power Platform CLI) and bundled with Webpack. For simpler controls we use vanilla TypeScript without React. We test with PCF Test Harness during development and browser DevTools for runtime debugging. Version control is Git (GitHub or Azure DevOps), and we deliver managed solutions (.zip) for import into your Dataverse environment.
How do I choose between Power Apps PCF development companies?
Compare on six factors rather than on rate. Who supports the control after go live and under what contract. Who owns the source, the managed solution, and the documentation when the engagement ends. Whether delivery depends on one person or on a team. Who screened the engineer for Dataverse and solution ALM as well as for TypeScript and React. Who owns the upgrade at each of the two Microsoft release waves a year. And what the commercial shape is: a fixed price after an approved specification is a very different purchase from an hourly rate against an unwritten scope. A repository of open source controls and an individual freelancer answer some of those and leave the rest to you. Solzet answers all six in the statement of work.
Why choose a specialist PCF consultancy over free components on GitHub?
Often you should not, and we say so on our own comparison guide. If a maintained open source control does what you need, take it, and check when it was last updated rather than how many stars it has. A consultancy earns its fee where that breaks down: when the control has to know your business rules, when the form it sits on is business critical, when nobody in house can adapt or repackage the component, or when something has to survive the next release wave with an owner attached. What you are buying then is not the code, which is the cheap part. It is accountability for the code: a support arrangement, defined IP ownership, and a team rather than an issues tab.
Can Solzet take over or fix a PCF control built by someone else?
Yes, and it is a regular part of our PCF work. We start with an audit rather than a rewrite: the manifest and property definitions, what runs in init against what is repeated on every updateView, whether state is released in destroy, how the control behaves against a real dataset with paging and a restricted user, and how the solution is versioned and packaged. That produces a written picture of what you have, what is actually wrong, and the smallest change that fixes it, so a rebuild is a decision you take with evidence rather than a default. Controls left behind by a freelancer who is no longer reachable, or by a partner who has stepped away, are the common cases.
What are the steps in the PCF control development lifecycle?
Four phases. Planning: agree the host, the control type, standard against virtual, the data access route, and the ControlManifest.Input.xml, which is the contract the platform actually reads. Coding: expensive work once in init, cheap idempotent rendering in updateView, a pure read in getOutputs, and full cleanup in destroy. Testing: the local harness for layout and interaction, then a real environment at real volume opened by a non-administrator, because the harness has no Dataverse, no security roles, and no realistic paging. Deployment: a versioned managed solution built from committed source, imported into test before production, published, verified on the real form, with a rollback artifact kept and an owner named for the next release wave.
How do you test a PCF control before deploying it?
Start in the local test harness with npm start watch for layout, resize, and interaction, then move everything that involves data or permissions into a real development environment. The harness cannot reproduce the failures that matter: context.webAPI is unavailable, security roles and field level security do not apply, paging does not behave like a real view, and there is no form context. The highest value test is opening the form as a test user with the real production security role rather than as a system administrator, followed by testing at the worst case row count from the specification, on the mobile app, on read only and inactive records, and with keyboard only navigation.
What should be on a PCF deployment checklist?
Increment the version in ControlManifest.Input.xml and in the solution. Build managed and unmanaged artifacts from the same clean build of committed source. Confirm the publisher and prefix match the environments. Confirm every table, column, view, and choice the control assumes is present in the target or ships in the same solution. Import the managed solution into a test environment that resembles production first. Publish all customizations, hard refresh, then verify on the real form as a real user rather than trusting the import log. Keep the previous managed artifact so rollback is possible. Re-measure form load in production. Complete handover of source, manifest decisions, and configuration properties, and name the owner for the next release wave.
Why does my PCF control not update after importing the solution?
Almost always the version. If the version in ControlManifest.Input.xml was not incremented, the platform can keep serving the cached bundle, so the import succeeds and the form shows the previous behaviour. Increment the manifest version and the solution version together, publish all customizations after the import, then hard refresh the browser. The other common causes are a dependency the development environment happened to have and the target does not, the same control shipped in two solutions where the last managed import wins the layer, and a feature such as the Web API that was never declared in the manifest, which makes context.webAPI undefined at runtime rather than failing at build time.
Should a PCF control be a standard control or a virtual control?
A virtual control borrows React and Fluent from the platform, so the bundle is much smaller, form load is faster, and the styling follows the host theme without extra work. That is the default we reach for on model-driven forms. A standard control carries its own rendering stack, which is the right call when you need a library the platform does not provide, when you are wrapping a third party visualisation or mapping SDK, or when you need a React version the platform library does not offer. The decision belongs in planning, because switching later is a rewrite of the rendering layer rather than a configuration change.
How do you develop a PCF control for Dynamics 365, and how long does it take?
Five steps, in this order. Run the build against buy decision tree, because configuring a first party feature or adopting a maintained component is often the correct answer. Write the specification and agree ControlManifest.Input.xml, which fixes the host, the control type, standard against virtual, data access and every configurable property. Build against the component lifecycle: expensive work once in init, cheap idempotent rendering in updateView, a pure read in getOutputs, full cleanup in destroy. Test in the local harness for layout, then in a real environment at real volume as a non-administrator. Deploy a versioned managed solution built from committed source, through test into production, with a rollback artifact kept. On the worked example above, a medium complexity dataset control with a third party renderer took nineteen working days, of which only eight were the build itself. A simple field control is considerably faster, and a large scheduler or map control considerably slower.
When should I build a PCF control instead of buying one?
Build or commission when the behaviour encodes your own process rather than a generic pattern, and when no maintained component fits with configuration alone. Buy or adopt when the behaviour is generic, which is most tagging, formatting, colour coding and viewer requirements. Configure and build nothing when a supported first party feature such as the modern grid, the editable grid, a first party map control or an embedded canvas app covers most of it. Customizing an existing component is the narrow middle case: worth it only when the licence permits a fork and the delta is small, because from that point you own the merge for every future update. Two further factors decide the route when the tree is close: how long the control has to live, and how many people it touches, since a control on a business critical form used by everybody justifies a written specification and a supplier who carries the risk.
How do I use a third party JavaScript library such as a charting engine in a PCF control?
There are four patterns. Borrow React and Fluent from the platform by declaring them as platform libraries in the manifest, which gives the smallest bundle and host theming. Bundle the library with npm and let the build pipeline put it inside bundle.js, which is self contained and works where outbound script loads are blocked. Externalise it with webpack and load it from a content delivery network, which keeps the bundle small and lets several controls share one cached copy, at the cost of network reachability and a content security policy allowance. Or keep it off the form entirely and do the work in a service when the library is heavy or needs a server side licence key. Whichever you choose, create the library instance once, update it in place on updateView, and dispose of it in destroy, or a busy form will flicker and leak. Settle commercial licensing before any code is written.
Can Solzet just review our existing PCF development process?
Yes. We are often asked to audit rather than to build: whether the manifest exposes the right configuration, whether work sits in the wrong lifecycle method, whether the dataset control handles paging honestly, whether destroy actually releases what init created, and whether the solution and versioning process gives you a rollback position. That produces a written picture of what you have and the smallest change that fixes it, which is a much cheaper starting point than a rebuild and is frequently what we recommend.
Related Services
Hire a Dynamics 365 Developer or CRM Developer in Armenia
Hire a senior Dynamics 365 developer or CRM developer from Solzet in Yerevan, Armenia: Dataverse, C# plugins, PCF controls, integrations and project rescue.
Hire a Power Platform Developer or Power Apps Developer
Hire a senior Power Platform developer from Solzet in Yerevan, Armenia: canvas and model-driven Power Apps, Power Pages portals, Power Automate, Dataverse, PCF.
Dynamics 365 Subcontracting & White-Label Services for Microsoft Partners
White-label Dynamics 365 subcontracting for Microsoft partners. Nearshore developers from Armenia - PCF, D365 CE, Power Automate. NDA-protected.
Ready to start?
Ready to start? Contact Solzet for a free consultation - we respond within one business day.
Get in Touch