# How to quickly build a scanning feature?

**URL:** <https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855>\
**Category:** Extensibility\
**Tags:** medical-devices, ui, v11.3\
**Created:** [September 15, 2026, 8:42am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855 "2026-09-15T08:42:24Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![Erchuang\_Shang](https://avatars.discourse-cdn.com/v4/letter/e/f14d63/32.png) [@Erchuang\_Shang](https://community.criticalmanufacturing.com/u/Erchuang_Shang)\
**Post date:** [September 15, 2026, 8:42am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855/1 "2026-09-15T08:42:25Z")

</div>

## Introduction

Although the product offers out-of-the-box scanning capabilities, additional development is required to deploy them as an independent feature in a production environment. In the following section, I will demonstrate how I utilized the existing functionalities to rapidly develop a working scanning solution, along with a summary of the challenges faced during development.

## Requirements

Shop floor operators currently need to navigate the full MES interface to perform the most frequent and time-critical operations at a Resource: **Check-In** , **Check-Out** , **Dispatch** , **Track-In** , and **Track-Out** , etc. On the shop floor, operators work with handheld Zebra devices that have a built-in scanner but a small screen, making the standard MES interface impractical to use efficiently. A lightweight, scan-driven Operator Cockpit tailored for the Zebra device enables operators to perform these core operations quickly, accurately, and without relying on a desktop terminal, reducing wait times, minimizing navigation steps, and keeping operators focused on the physical work at hand.  
The main requirement points are as follows:

1. Scan to retrieve Resource or Material:

2. Barcode Input: Support for manual barcode entry.  

## 1. How to ensure a Resource is scanned first?

**Question:**  
Since a scan could be either a Resource or a Material, but scanning a Resource first is a prerequisite for scanning a Material to be meaningful, how can we ensure that the first scan is always a Resource?

**Solution:**  
This feature implements the scanning logic via DEE. Upon the first scan, the result is forwarded to DEE, which evaluates the input through the following code to determine if it is a Resource, a Material, or an error.

```auto
/// <summary>
/// Gets the Resource or Material by name or barcode.
/// </summary>
/// <param name="input">The input.</param>
/// <returns>Resource or Material.</returns>
public (IResource, IMaterial) GetResourceOrMaterialByBarcode(string barcode)
{
    long? materialId = null;
    long? resourceId = null;
    IMaterial material = _entityFactory.Create<IMaterial>();
    IResource resource = _entityFactory.Create<IResource>();

    #region Resource query

    if (resource.ObjectExists(barcode))
    {
        resource.Load(barcode);
    }
    else if (material.ObjectExists(barcode))
    {
        material.Load(barcode);
        if (material.UniversalState != UniversalState.Terminated && !material.IsTemplate)
        {
            material.LoadReferences([Cmf.Navigo.Common.Constants.Product, Cmf.Navigo.Common.Constants.Step]);
        }
    }
    else
    {
        IEntityType materialEntityType = _entityFactory.Create<IEntityType>();
        materialEntityType.Load(Cmf.Navigo.Common.Constants.Material);

        IEntityType resourceEntityType = _entityFactory.Create<IEntityType>();
        resourceEntityType.Load(Cmf.Navigo.Common.Constants.Resource);

        bool isToSearchForBarcode = materialEntityType.AllowBarcodeIds || resourceEntityType.AllowBarcodeIds;
        if (isToSearchForBarcode)
        {
            IGenericTable entityTagGenericTable = _entityFactory.Create<IGenericTable>();
            entityTagGenericTable.Load("EntityTag");

            IFilterCollection entityTagFilters = new FilterCollection
            {
                new Filter
                {
                    Name = "Scope",
                    Operator = FieldOperator.IsEqualTo,
                    Value = (int) EntityTagScope.BarcodeId,
                    LogicalOperator = LogicalOperator.AND
                },
                new Filter
                {
                    Name = "Tag",
                    Operator = FieldOperator.IsEqualTo,
                    Value = barcode,
                },
            };

            entityTagGenericTable.LoadData(entityTagFilters);
            if (entityTagGenericTable.HasData)
            {
                DataSet entityTagDataSet = NgpDataSet.ToDataSet(entityTagGenericTable.Data);
                DataRow row = entityTagDataSet.Tables[0].Rows[0];
                string entityType = row.Field<string>("EntityType");
                if (entityType == Cmf.Navigo.Common.Constants.Material)
                {
                    materialId = row.Field<long>("EntityId");
                }
                else if (entityType == Cmf.Navigo.Common.Constants.Resource)
                {
                    resourceId = row.Field<long>("EntityId");
                }
                if (materialId.HasValue && material.ObjectExists(materialId.Value))
                {
                    material.Load(materialId.Value);
                    if (material.UniversalState != UniversalState.Terminated && !material.IsTemplate)
                    {
                        material.LoadReferences([Cmf.Navigo.Common.Constants.Product, Cmf.Navigo.Common.Constants.Step]);
                    }
                }
                else if (resourceId.HasValue && resource.ObjectExists(resourceId.Value))
                {
                    resource.Load(resourceId.Value);
                }
            }
        }
    }

    #endregion Barcode query

    return (resource, material);
}

```

The page also passes the Resource parameter to the DEE. During the initial scan, this parameter is null. If the scanned information is not a Resource, an error will be returned. If a valid Resource is scanned, it will be returned and stored. For all subsequent scans, the Resource parameter passed to the DEE will be populated.

> ⚠ NOTE  
> When passing the Resource parameter to DEE, you must pass the `Resource Name` instead of the Resource entity object. Passing the object is prohibited because DEE reloads and returns a new Resource instance on every call. Even if it represents the same Resource, the object reference changes, which triggers continuous calls to DEE. Therefore, pass the `Resource Name` instead. Since it is a static string that remains unchanged as long as the Resource is not switched, it will not trigger redundant DEE calls.

## 2. How can a Material be selected or deselected via scanning?

This feature uses a Grid component to display data. The Grid component has a `Selected` input parameter and a `Changed Selection` output parameter, both of which are array types. The `Selected` input parameter is used to set the selected or unselected state of the data in the Grid, while the `Changed Selection` output parameter retrieves the currently selected data.

We need to pass both the `Changed Selection` output parameter and the scanned Material to DEE simultaneously. Within DEE, the `Changed Selection` array is evaluated: if the scanned Material is present in the array, it will be removed; if it is not present, it will be added. Finally, the updated `Changed Selection` array is returned from DEE to the page and reassigned to the `Selected` input parameter. This mechanism enables the select and deselect functionality via scanning.

> ⚠ NOTE  
> When passing the `Changed Selection` output parameter to DEE, we need to convert all Materials in the array into a single string. This can be achieved by concatenating the Material Names with a semicolon(‘;’) as the delimiter. This prevents DEE from being called in a continuous loop. If the `Changed Selection` array object were passed directly, any changes to the `Selected` input parameter would trigger a change in the `Changed Selection` output object, resulting in an infinite loop of DEE calls.

**Solution:**  
A dedicated Converter component was created to concatenate the Material array into a single string using semicolons(‘;’) as delimiters. Upon receiving this parameter, DEE splits the string back by semicolons to determine whether the scanned Material is present in the array.

```TypeScript
@Converter({
  name: $localize`:@@cmf-meddev-core/handle-materials-to-name-string#NAME:Handle Materials To Name String Converter`,
  input: [
    <ComplexType>{
      type: null,
      CollectionType: CollectionType.Array,
    },
  ],
  output: SimpleType.String,
})
@Pipe({
  standalone: true,
  name: 'handleMaterialsToNameString',
})
export class HandleMaterialsToNameStringConverter implements PipeTransform {
    transform(value: any[], ...args: any[]): any {
        
    let result = '';
    if (value && value.length > 0) {
      value.forEach((n) => {
        if (n instanceof Cmf.Navigo.BusinessObjects.Material) {
          result += n.Name + ';';
        }
      });
    }
    return result;
  }
}

```

 ![mobilecockpit_materialnameconverter01](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/d/db0f3a363612a9b348fbf15f2de11ba5ac87638e.png)  
**Image:** _Material Name Converter_

**Trivial Issue:**  
In MES system v11.3.3, when operating in Mobile mode, scanning to select an item does not actually select the corresponding data in the interface. The result is shown in the figure below:

 ![mobilecockpit_selectedissue](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/f/fafa11a939ffde4b704f544eb3b348240f684cf0.png)  
**Image:** _Automatic select Issue_

After communicating with the Product team, this has been confirmed as a product bug. However, the issus can be resolved using the following workaround: The results to be displayed in the Grid must be passed to DEE simultaneously. After logical processing, the `Selected` parameter is output. It is crucial to ensure that the `Selected` parameter shares the exact same reference as the data source displayed in the Grid. In other words, the `Selected` parameter must not be passed out after Grid’s data source has been loaded. This approach will resolve the current auto-selection issue.

 ![mobilecockpit_outputselectedmaterials](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/e/e552d50d60c00390e8ba4d795460dfafd6433055.png)  
**Image:** _Selected Materials_

## 3. How can add a insert barcode button?

User also require the option to enter information manually instead of scanning.  
The `Barcode Identification` page under the `Manufacturing` menu has already implemented this feature.

 ![mobilecockpit_barcodeidentification](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/d/d7671ca8c023c8f9932f478f5beb163f330873d7.png)  
**Image:** _Barcode Identification_

Since this page also built using UIPage, we can use it as a direct reference. This feature utilizes the native system button `Manufacturing.BarcodeIdentification.ManualInsert` to open a dialog box for barcode entry.

 ![mobilecockpit_barcodeidentification02](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/4/4200032f32995b4b8d36c2c732368f47889d66a2.png)  
**Image:** _Insert Barcode Button_

It then uses the `Barcode Actions` widget to capture the input. The `Barcode Actions` widget provides a simple way to scan barcode(line or QR codes) that match specific objects within the system. Since we are only targeting `Resource` and `Material`, we just need to provide these two options in the `Entities` tab.

 ![mobilecockpit_barcodeidentification02](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/4/4200032f32995b4b8d36c2c732368f47889d66a2.png)  
**Image:** _Barcode Actions_

**Question:**

1. Since the `Barcode Actions` widget does not need to be displayed on the page and is only used to capture input, we need to hide it from the UI.
2. The current feature uses the system `Barcode` task to retrieve scanned content as a string, which is then passed to DEE. However, the `Barcode Actions` widget outputs an entity. We need to convert this output into a string to maintain a consistent data format before passing it to DEE in the same manner.

**Solution:**

1. To address the first issue, we can use the following Less code in the file `/Cmf.MedDev.HTML/src/styles.less` to hide the `Barcode Actions` component.

2. For the second issue, we need to use a Converter `Get Entity Name` to retrieve the Name of the entity outputted by the `Barcode Actions` widget.  

## Others

Since the `title` and `subtitle` of the UIPage cannot be displayed in Mobile mode, we need to develop a new widget to display this information. The front-end code is as follows:

 ![mobilecockpit_subtitleinpc](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/2/23c86bb3b8c268ca3584951b70c1e4bb234fc22e.png)  
**Image:** _Entity Additional Info in ResourceView_

**HTML:**

```HTML
<div class="base-page-header">
  <div class="title-bar base-page-title">
    <cmf-core-controls-page-title>
      <div class="page-icon icon-mes-et-sm-resource"></div>
      <div class="page-label">{{ title }}</div>
      <div class="page-sublabel">{{ subTitle }}</div>
      <div class="page-title-additional-info" [innerHTML]="safeHtml"></div>
    </cmf-core-controls-page-title>
  </div>
</div>

```

**TypeScript:**

```TypeScript
...
@Widget({
  name: $localize`:@@cmf-meddev-core/entity-additional-info-widget#NAME:Entity Additional Info Widget`,
  iconClass: 'icon-core-st-lg-generic',
  settingsComponent: {
    component: EntityAdditionalInfoWidgetSettingsComponent,
  },
  inputs: {
    title: {
      type: WidgetValueType.String,
    },
    subTitle: {
      type: WidgetValueType.String,
    },
    additionalInfo: {
      type: WidgetValueType.String,
    },
  },
})
@Component({
  standalone: true,
  selector: 'cmf-meddev-core-entity-additional-info-widget',
  imports: [CommonModule],
  templateUrl: './entity-additional-info-widget.component.html',
  styleUrl: './entity-additional-info-widget.component.less',
  inputs: ['title', 'subTitle', 'additionalInfo'],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class EntityAdditionalInfoWidget
  extends WidgetGeneric
  implements WidgetRepresentation
{
  /**
   * Defines how the widget will be represented
   */
  public uiWidgetViewMode: WidgetViewMode = WidgetViewMode.Normal;

  @Input() public title: string = '';
  @Input() public subTitle: string = '';
  @Input() public additionalInfo: string = '';

  /**
   * Constructor
   */
  constructor(
    viewContainerRef: ViewContainerRef,
    elementRef: ElementRef,
    feedback: FeedbackService,
    private sanitizer: DomSanitizer,
  ) {
    super(viewContainerRef, elementRef, feedback);
  }

  get safeHtml(): SafeHtml {
    return this.sanitizer.bypassSecurityTrustHtml(this.additionalInfo);
  }
}

```

**Less:**

```Less
cmf-core-controls-base-page .base-page-header {
  background-color: #fafafa;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

```

 ![mobilecockpit_entityadditionalinfowidget](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/2/2b99c379caaa2caba32f2cf4423b819ef0fd8fe1.png)  
**Image:** _Entity Additional Info Widget_

It looks like this:

 ![mobilecockpit_entityadditionalinforesult](https://canada1.discourse-cdn.com/flex050/uploads/community_criticalmanufacturing/original/2X/8/88b264f6728eaed169a2b3ce4b63c6db6177c0e7.png)  
**Image:** _Entity Additional Info Widget_

---

<div class="post-metadata">

**Author:** ![Leonor\_Marques](https://yyz2.discourse-cdn.com/flex050/user_avatar/community.criticalmanufacturing.com/leonor_marques/32/1074_2.png) [@Leonor\_Marques](https://community.criticalmanufacturing.com/u/Leonor_Marques)\
**Post date:** [September 15, 2026, 8:50am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855/2 "2026-09-15T08:50:13Z")

</div>

@Joao_Moreira and @Jorge_Bertocchini can you check if we can use this resolutions in the documentation maybe as a developer tutorial?

---

<div class="post-metadata">

**Author:** ![Jorge\_Bertocchini](https://yyz2.discourse-cdn.com/flex050/user_avatar/community.criticalmanufacturing.com/jorge_bertocchini/32/1135_2.png) [@Jorge\_Bertocchini](https://community.criticalmanufacturing.com/u/Jorge_Bertocchini)\
**Post date:** [September 15, 2026, 9:04am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855/3 "2026-09-15T09:04:25Z")

</div>

I’d say they’re good enough, I’m adding this to our backlog, excellent stuff! 🙂

---

<div class="post-metadata">

**Author:** ![Joao\_Moreira](https://avatars.discourse-cdn.com/v4/letter/j/b9bd4f/32.png) [@Joao\_Moreira](https://community.criticalmanufacturing.com/u/Joao_Moreira)\
**Post date:** [September 15, 2026, 9:15am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855/4 "2026-09-15T09:15:50Z")

</div>

Sure, we’ll add it to our backlog and keep you updated on our progress.

Nice work, @Erchuang_Shang !

---

<div class="post-metadata">

**Author:** ![system](https://yyz2.discourse-cdn.com/flex050/user_avatar/community.criticalmanufacturing.com/system/32/23_2.png) [@system](https://community.criticalmanufacturing.com/u/system)\
**Post date:** [September 17, 2026, 9:16am UTC](https://community.criticalmanufacturing.com/t/how-to-quickly-build-a-scanning-feature/855/5 "2026-09-17T09:16:35Z")

</div>

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.
