Providers
Providers are the runtime components that supply format and unit definitions to formatters and parsers. Understanding the different provider types and how to register them is essential for setting up quantity formatting in iTwin.js applications.
Understanding Providers
UnitsProvider
The UnitsProvider interface is central to unit management in iTwin.js. It provides methods to:
Units Provider Concept
A units provider acts as a registry and converter for units. When you need to format or parse a quantity value, the provider:
- Locates the source unit (e.g., meters for persistence)
- Locates the target unit(s) (e.g., feet and inches for display)
- Provides conversion factors between these units
- Validates unit compatibility (ensures units are in the same phenomenon)
BasicUnitsProvider
BasicUnitsProvider is a standalone provider that contains common units needed for basic quantity formatting. It's used as the default provider in IModelApp.quantityFormatter when no iModel is open.
Characteristics:
- No dependencies on iModels or schemas
- Contains units for: length, angle, area, volume, time
- Sufficient for applications without schema-specific units
- Lightweight and fast
When to use:
- Applications without iModel dependencies, enabled by default
- Simple formatting scenarios
- Fallback when schema loading fails
SchemaUnitProvider
SchemaUnitProvider loads unit definitions from EC schemas stored in iModels. It provides access to the extensive Units schema as well as custom units defined in domain schemas.
Characteristics:
- Requires access to ECSchemas via SchemaContext, commonly through iModels
- Accesses units through
SchemaContext
- Supports custom domain-specific units
- More comprehensive than BasicUnitsProvider
When to use:
- Applications working with iModels
- Need for domain-specific units (civil, structural, etc.)
- When unit definitions must match schema specifications
FormatsProvider
A FormatsProvider supplies format definitions for KindOfQuantities. The FormatDefinition interface extends FormatProps to help identify formats.
SchemaFormatsProvider
SchemaFormatsProvider retrieves formats from EC schemas using a SchemaContext. It requires a UnitSystemKey to filter formats according to the current unit system.
Characteristics:
- Loads formats from KindOfQuantity definitions in schemas
- Filters formats by unit system (metric, imperial, usCustomary, usSurvey)
- Throws error for invalid EC full names
- Read-only format provider
Example: Simple Formatting
Formatting with SchemaFormatsProvider
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric");
const unitsProvider = new SchemaUnitProvider(schemaContext);
const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m");
// No unit system was provided, and no format was found in the cache so the method will return the first presentation format for the KoQ, which uses KM.
const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH");
const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!);
const formatSpec = await FormatterSpec.create("TestSpec", format, unitsProvider, persistenceUnit);
const result = formatSpec.applyFormatting(50); // The persistence unit is meters, so this input value is 50 m.
// result in formatted value of 50 m
Example: Parsing
Parsing with SchemaFormatsProvider
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric");
const unitsProvider = new SchemaUnitProvider(schemaContext);
const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m");
const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH_LONG");
const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!);
const parserSpec = await ParserSpec.create(format, unitsProvider, persistenceUnit);
const result = parserSpec.parseToQuantityValue("50 km");
// result.value 50000 (value in meters)
Example: Unit System Override
When retrieving a format from a schema, you might want to ensure the format matches your current unit system. You can pass the unit system on initialization or change it afterward:
Formatting with Unit System Override
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric");
const unitsProvider = new SchemaUnitProvider(schemaContext);
const persistenceUnit = await unitsProvider.findUnitByName("Units.M"); // or unitsProvider.findUnit("m");
formatsProvider.unitSystem = "imperial"; // This will cause the method to return the first presentation format for the KoQ that uses imperial units.
const formatProps = await formatsProvider.getFormat("AecUnits.LENGTH_LONG");
const format = await Format.createFromJSON("testFormat", unitsProvider, formatProps!);
const formatSpec = await FormatterSpec.create("TestSpec", format, unitsProvider, persistenceUnit);
const result = formatSpec.applyFormatting(50); // The persistence unit is meters, so this input value is 50 m.
// result in formatted value of 164'0 1/2"
Example: Retrieving KindOfQuantity and Persistence Unit
When you only have a KindOfQuantity name, you can use a SchemaContext to find the schema item and access its persistence unit:
Using SchemaContext to get KindOfQuantity and persistence unit
const formatsProvider = new SchemaFormatsProvider(schemaContext, "metric");
const unitsProvider = new SchemaUnitProvider(schemaContext);
const kindOfQuantityName = "AecUnits.LENGTH";
// Get the format definition
const formatDef = await formatsProvider.getFormat(kindOfQuantityName);
if (!formatDef)
throw new Error(`Format not found for ${kindOfQuantityName}`);
const kindOfQuantity = await schemaContext.getSchemaItem(kindOfQuantityName, KindOfQuantity);
if (!kindOfQuantity)
throw new Error(`KindOfQuantity not found for ${kindOfQuantityName}`);
const persistenceUnit = kindOfQuantity.persistenceUnit;
if (!persistenceUnit)
throw new Error(`Persistence unit not found for ${kindOfQuantityName}`);
const persistenceUnitProps = await unitsProvider.findUnitByName(persistenceUnit.fullName);
const format = await Format.createFromJSON(formatDef.name ?? "", unitsProvider, formatDef);
const formatterSpec = await FormatterSpec.create(
formatDef.name ?? "",
format,
unitsProvider, // Use a schema units provider
persistenceUnitProps
);
const _formattedValue = formatterSpec.applyFormatting(123.45);
MutableFormatsProvider
MutableFormatsProvider extends the read-only FormatsProvider by allowing formats to be added or removed at runtime.
Characteristics:
- Supports dynamic format management
- Can add custom formats not in schemas
- Can override schema-defined formats
- Fires
onFormatsChanged event when formats are modified
Example: Implementation
Example MutableFormatsProvider implementation
/**
* Implements a formats provider with a cache, to allow adding/removing formats at runtime.
*/
class ExampleFormatProvider implements MutableFormatsProvider {
private _cache: Map<string, FormatDefinition> = new Map();
public onFormatsChanged = new BeEvent<(args: FormatsChangedArgs) => void>();
public async getFormat(name: string): Promise<FormatDefinition | undefined> {
return this._cache.get(name);
}
public async addFormat(name: string, format: FormatDefinition): Promise<void> {
this._cache.set(name, format);
this.onFormatsChanged.raiseEvent({ formatsChanged: [name]});
}
public async removeFormat(name: string): Promise<void> {
this._cache.delete(name);
this.onFormatsChanged.raiseEvent({ formatsChanged: [name]});
}
}
Example: Adding Formats
Adding formats to MutableFormatsProvider
const formatsProvider = new ExampleFormatProvider();
const format: FormatDefinition = {
label: "NewFormat",
type: "Fractional",
precision: 8,
formatTraits: ["keepSingleZero", "showUnitLabel"],
uomSeparator: "",
};
await formatsProvider.addFormat("DefaultToolsUnits.LENGTH", format);
const retrievedFormat = await formatsProvider.getFormat("DefaultToolsUnits.LENGTH");
// retrievedFormat is the format we just added.
FormatSetFormatsProvider
FormatSetFormatsProvider manages formats within a FormatSet. This provider automatically updates the underlying format set when formats are added or removed, making it ideal for applications that need to persist format changes.
Key Features:
- String Reference Resolution: Automatically resolves string references to their target FormatDefinition. When a format references another via string (e.g.,
"DefaultToolsUnits.LENGTH": "CivilUnits.LENGTH"), the provider resolves and returns the actual FormatDefinition.
- Chain Resolution: Supports chains of references with circular reference detection.
- Cascade Notifications: When adding or removing a format, the
onFormatsChanged event includes not only the modified format but also all formats that reference it (directly or indirectly).
- Fallback Provider: String references can resolve through an optional fallback provider if the target format isn't found in the format set.
Example: FormatSet with String References
Using FormatSetFormatsProvider with string references
const unitsProvider = new SchemaUnitProvider(schemaContext);
const persistenceUnit = await unitsProvider.findUnitByName("Units.M");
// Create a format set with a base format and string references
const formatSet = {
name: "MyFormatSet",
label: "My Custom Formats",
unitSystem: "metric" as const,
formats: {
// Base format definition
"CivilUnits.LENGTH": {
composite: {
includeZero: true,
spacer: " ",
units: [{ label: "m", name: "Units.M" }]
},
formatTraits: ["keepSingleZero", "showUnitLabel"],
precision: 2,
type: "Decimal"
} as FormatDefinition,
// DISTANCE references LENGTH via string
"DefaultToolsUnits.LENGTH": "CivilUnits.LENGTH",
}
};
// Create the provider
const formatsProvider = new FormatSetFormatsProvider({ formatSet });
// Getting AecUnits.LENGTH resolves to the RoadRailUnits.LENGTH format definition
const lengthFormat = await formatsProvider.getFormat("DefaultToolsUnits.LENGTH");
const format = await Format.createFromJSON("length", unitsProvider, lengthFormat!);
const formatSpec = await FormatterSpec.create("LengthSpec", format, unitsProvider, persistenceUnit);
const result = formatSpec.applyFormatting(42.567);
// result is "42.57 m"
AlternateUnitLabelsProvider
AlternateUnitLabelsProvider allows specifying alternate labels for units during parsing. This is useful for:
- Supporting common abbreviations (e.g., "ft" and "foot" for feet)
- Enabling easier keyboard input (e.g., "^" for degrees "°")
- Accommodating regional variations in unit labels
Registering Providers in iTwin Applications
This section covers how to register and configure providers in your iTwin application. Proper provider registration ensures consistent quantity formatting across your application.
Registering UnitsProvider
Manual Registration
You can manually register a SchemaUnitProvider when opening an iModel:
Manual SchemaUnitProvider registration
/** Register SchemaUnitProvider when IModelConnection is opened */
export async function registerSchemaUnitProvider(iModelConnection: IModelConnection) {
const schemaLocater = new ECSchemaRpcLocater(iModelConnection);
await IModelApp.quantityFormatter.setUnitsProvider(new SchemaUnitProvider(schemaLocater));
}
Automatic Registration on IModelConnection Open
The recommended approach is to automatically register the provider when any IModelConnection opens:
Automatic registration via IModelConnection.onOpen
/** Register SchemaUnitProvider automatically when IModelConnection opens */
export function setupIModelConnectionListener() {
IModelConnection.onOpen.addListener(async (iModelConnection: IModelConnection) => {
const schemaLocater = new ECSchemaRpcLocater(iModelConnection);
await IModelApp.quantityFormatter.setUnitsProvider(new SchemaUnitProvider(schemaLocater));
});
}
If errors occur while configuring the units provider, they are caught within the QuantityFormatter.setUnitsProvider method, and the code reverts back to BasicUnitsProvider.
Registering FormatsProvider
Using SchemaFormatsProvider
Register a SchemaFormatsProvider to load formats from iModel schemas:
Registering SchemaFormatsProvider on IModelConnection open
const removeIModelConnectionListener = IModelConnection.onOpen.addListener((iModel: IModelConnection) => {
if (iModel.isBlankConnection()) return; // Don't register on blank connections.
const schemaFormatsProvider = new SchemaFormatsProvider(iModel.schemaContext, IModelApp.quantityFormatter.activeUnitSystem);
const removeUnitSystemListener = IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener((args) => {
schemaFormatsProvider.unitSystem = args.system;
});
iModel.onClose.addOnce(() => {
removeUnitSystemListener();
});
IModelApp.formatsProvider = schemaFormatsProvider;
});
IModelConnection.onClose.addOnce(() => {
removeIModelConnectionListener();
IModelApp.resetFormatsProvider();
});
Using FormatSetFormatsProvider
For applications that persist user format preferences:
// Load FormatSet from application settings
const formatSet: FormatSet = await loadUserFormatPreferences();
// Create provider with optional fallback
const fallbackProvider = new SchemaFormatsProvider(schemaContext);
const formatSetProvider = new FormatSetFormatsProvider(formatSet, fallbackProvider);
// Register with IModelApp
IModelApp.formatsProvider = formatSetProvider;
// Listen for changes to update persistence
formatSetProvider.onFormatsChanged.addListener((formats) => {
// Save updated format set to user preferences
saveUserFormatPreferences(formatSet);
});
Adding Alternate Unit Labels
Add alternate unit labels for easier input during parsing:
Adding alternate unit labels
/** Add alternate unit labels for easier input during parsing */
export function addAlternateUnitLabels() {
// Use "^" as an alternate label for degrees
IModelApp.quantityFormatter.addAlternateLabels("Units.ARC_DEG", "^");
// Add alternate labels for feet
IModelApp.quantityFormatter.addAlternateLabels("Units.FT", "feet", "foot");
}
Configuring Unit System
Set the active unit system for the QuantityFormatter:
Configuring unit system
/** Configure the unit system for the QuantityFormatter */
export async function configureUnitSystem() {
// Set the active unit system (metric, imperial, usCustomary, or usSurvey)
await IModelApp.quantityFormatter.setActiveUnitSystem("metric");
}
See Also