How to implement a component

After code migration, it may happen that a control is migrated as a stub. In this case, the migrated code will look like this:

import { Stub_System_Windows_Controls_Slider} from "stubs";
...
export class MyControl extends UserControlModel {
   ...
   public slider : Stub_System_Windows_Controls_Slider = null;
   ...
}

A stub does not have any functionality build in. It is just a piece of code which allows the migrated code to be compiled without errors.

This means that wms-framework and i-components don't have support for the Silverlight control, yet. This guide explains how to add support for a new control, i.e. how to implement a new component, and how to use the new component to replace the stub.

Source code

The source code for the Silverlight example and migrated application used through this guide is available on the resources section.

Slider example

Lets start with a small example: a Silverlight application which uses a Slider control. The application also uses a Grid, a couple of TextBlocks and a Button. The application looks like this:

SliderTest application.

On XAML, the code looks like this:

The Button has a function associated with the click event. We can find that function on the C# control code:

When the user clicks on the button, a message box should appear showing the current value on the slider.

Message showing a Value of 0.

If the slider is moved and the Show button is clicked again, a different value should be displayed:

Message showing a Value of 0.36...

At the time of this writing, Slider is not supported by wms-framework and i-components. Therefore, when the previous code is migrated, that Slider will be converted into a stub, both on the Angular component and the model:

The first step to support Slider is to create a model for it.

Create a model

A model must be created to represent the Slider control on the migrated application. This model should behave as close as possible to the original Slider. However, it is not necessary to implement all members of the original control. Only the members used by the legacy application need to be implemented.

On the components repository, on folder projects\wms-framework\src\lib\models\controls\, create a file named SliderModel.ts :

Most controls on Silverlight inherit from Control, either directly or indirectly. Slider inherits from RangeBase, so SliderModel should inherit from RangeBaseModel:

There is an issue: RangeBaseModel is not implemented. There are two options: skip the middleman and inherit directly from Control, or create RangeBaseModel and every other model needed in the inheritance chain up to Control.

Inheritance chain: skip RangeBaseModel, or implement it.

Let's follow the second approach and create RangeBaseModel:

On Silverlight RangeBase inherits from Control, so there is no need to create any more models to complete the inheritance chain.

Now let's add some properties to SliderModel. The Slider control has a lot of members, like IsFocused, Orientation, OnMouseEnter, etc. Those members are not used in the legacy code, it is a waste of resources to implement them. Focus instead on members used by the legacy application, like Value, which is a property used in the btnShow_Click() function. In Silverlight Value is not declared on Slider, but it is inherited from RangeBase, so a good approach is to implement it on RangeBaseModel as well:

The data type for Value is double on Silverlight, but number is used on the model. TypeScript does not have a double data type, the closest available data type is number (check JavaScript data types). This should be OK for most applications, but be aware that behavior could be different for other types (for example, float on C# and number on TypeScript have different limits).

Now add RangeBaseModel and SliderModel to the public API of wms-framework, in the projects\wms-framework\src\lib\models\index.ts file:

Re-build wms-framework and copy it into the migrated application folder (check how to do this here). Now replace all uses of Stub_System_Windows_Controls_Slider with SliderModel in the migrated application:

After building the migrated application, there should be no compilation errors.

SliderModel needs a way to be rendered on the browser, and to let the user interact with it. It is time to create a component for SliderModel.

Create a component

Given that an Angular component is made up of several files, it makes sense to create a folder to group those files. Under folder projects\i-components\src\lib\components create a new folder called slider, with three files inside: slider.component.html, slider.component.scss and slider.component.ts.

On slider.component.ts, let's create a simple Angular component class:

Note the wm-slider selector, we will use this selector to replace the <stub-wm-slider> on the migrated application.

Most of the component class is pretty common Angular stuff, except for ComponentId and BaseComponent.

BaseComponent is an Angular pseudo-component which provides a lot of functionality to deal with models and Angular lifecycles. This will be covered along the way.

ComponentId is a wms-framework directive which helps identify Angular components in cases of dynamic instantiation, i.e. when a component is created on the fly by the code behind. For now, just add a unique identifier into projects\wms-framework\src\lib\helpers\AngularComponentId.ts file:

Note that since AngularComponentId.ts lives in wms-framework, you have to build wms-framework and copy it into i-components for the changes to be reflected on the i-components side.

Now let's work on the component template. The model should be represented on the screen as close to the legacy control as possible (functionally speaking, appearance issues will be explained on another guide). This means that for complex controls it may be necessary to use third party controls (like Infragistics' IgniteUI for Angular). Fortunately a simple slider input can be used in this case, which is already available on HTML:

This will render a slider on the screen, but there is no connection between the rendered slider and the component class, or the model. Lets start by connecting the model with the component.

Connect the component and the model

Open slider.component.ts and modify SliderComponent to add a model property:

Note that model is decorated with Input. This will allow to link the model using HTML, as will be shown later on.

What is modelProxy? During component initialization, it may be the case that some value needs to be assigned into the model, but the model is not ready yet. In that case, the value is stored into the model proxy, and later it is transferred into the model. In short, we need modelProxy for proper model initialization.

Speaking of model initialization, lets implement OnInit on the component to perform proper component (and model) initialization:

Lets break down the component initialization. On line 7 we make sure that the component has a valid model: if the model has not been assigned yet, a new model is created. On line 8 we call setupModel(), which is provided by BaseComponent, and it performs a lot of tasks, among them:

  • Copy values from modelProxy into model.

  • Redirect modelProxy to model: from here on both modelProxy and model point to the same object.

  • Setup the bindings mechanism on the model.

  • Setup the change detection mechanism: this allows the component to react to model changes.

On line 9 we call ngOnInit() on the BaseComponent, which will synchronize more stuff between the model and the component (like validation error mechanisms and tab behavior).

The slider component also needs a constructor:

On line 9 we call the constructor of the parent class (BaseComponent) and provide it with Angular's injector and change detector. BaseComponent needs those to synchronize the model with the component.

Connect the template with the model

Now that a model is available on the component, lets connect it with the input slider on the HTML template. First add a function on the component that receives a value from the input and sets that value on the model:

This function is quite simple: it just takes the given value and assign it into the Value property of the model. When handling bindings (Silverlight migrated bindings, not Angular bindings) this assignment becomes more complex, but for now it will do.

Now open the HTML template and call onInput() from the <input> element:

First we add a template variable for input (#sliderInput), which will be used when calling onInput(). Then use Angular event binding to listen for input events, and there call onInput() using sliderInput.value as parameter.

That's it. When the user moves the slider, Angular will trigger the input event, it will call onInput() on the slider component, and it will update the Value property on the model. Now lets export the component to be able to use it in the migrated application.

Export the component

To make the component available outside of i-components, the component must be added into the i-components module, and it must be added to the public API. Before going any further, make sure to have build wms-components and copy it into i-components, so that new models are available to i-components.

To add the slider component into i-components module, open projects\i-components\src\lib\conversion-support.module.ts file and add SliderComponent in the declarations and exports sections of the IComponentsModule:

To add the slider component into the public API, open projects\i-components\src\lib\components\index.ts and add a line to export the slider component file:

Rebuild and copy i-components into the migrated code to try out the slider component.

Slider component in action

After copying the modified i-components into the migrated code, open MainPage HTML template and replace <stub-wm-slider> with <wm-slider>:

Rebuild the migrated application, start it (with ng serve) and take a look at the slider:

Hmmm, that does not look right. Let's see if the slider works as expected by clicking the Show button:

That's not good either. Try moving the slider and clicking the Show button again:

That's a little better, at least it seems like the slider is having some effect on the model. But the value is out of range (in the legacy application a minimum of -1 and a maximum of 1 was defined).

So far the following issues can be identified:

  • Slider is in wrong position.

  • Initial value is undefined.

  • The minimum and maximum value don't match legacy application.

Lets fix those issues one by one.

Position

Looking closely at the legacy XAML code and comparing it with the HTML template, it can be seen that some information was lost during migration:

At first glance, HorizontalAlignment, VerticalAlignment, Margin, Minimum and Maximum can't be found on the HTML template. Margin can be found on the component style sheet, so it was not lost:

The other properties were lost during migration. This means that they must be inserted manually. Lets begin with HorizontalAlignment and VerticalAlignment, because those are the ones related to position:

Note that the value for horizontalAlignment and verticalAlignment is a string, that's why it is necessary to put double quotes inside single quotes ('"Left"'). It also works the other way around: single quotes inside double quotes ("'Top'").

Lets see the result:

Slider component with correct position, but wrong size.

Now the slider is in the correct position, but it does not have the correct size. The <wm-slider> on the HTML template already has a width and height defined, so the issue is not lost information this time.

Using the browser developer tools to inspect the <wm-slider> element, it can be seen that <wm-slider> has the correct width and height:

wm-slider element with correct width and height.

However the <input> element inside <wm-slider> does not respect this size, and grows beyond its container:

Input element is bigger than slider component.

The width and height from the slider component must be propagated into the input element. Open the slider component template and add a couple of Angular style bindings:

Now open slider component class to add inputWidth and inputHeight properties:

inputWidth takes the Width from the model and adds a 'px' at the end to use it as a style value in pixels. Same thing with inputHeight. Rebuild i-component, copy it into the migrated application and try again:

Slider component with correct position and size.

Great! Now the slider component has the correct position and size. Lets fix the slider initial value.

Initial value

If the Show button is clicked without having moved the slider, a "Value=undefined" message is shown. According to RangeBase documentation, the initial value should be 0:

RangeBase documentation for Value property.

Checking RangeBaseModel it can be seen that a initial value for Value was never set: nor in its declaration, and neither in the constructor (there is no constructor):

Open RangeBaseModel and set a initial value of 0 for the backing field of Value:

Rebuild wms-components, copy it into the migrated application and try clicking on the Show button without moving the slider:

Slider with correct initial value.

Excellent! The initial value is 0, as it should be. Now lets fix the last issue: the minimum and maximum values.

Minimum and Maximum

Remember that Minimum and Maximum were among the properties lost during migration. Lets begin by adding them in the migrated application:

However, when the migrated application is build the following error is shown:

Error when adding minimum information.

This error means that we need to add a minimum input (an Angular input, not an HTML input element) on the slider component. Similar for maximum. When HorizontalAlignment and VerticalAlignment information were added, this error did not happen because BaseComponent already provides those input properties, and the slider component inherits those inputs. But no one provides minimum and maximum, so it is the responsibility of slider component to provide them.

Open the slider component class and add inputs for minimum and maximum:

Note that modelProxy is used instead of model to get and set the minimum and maximum values. This is because during component initialization the model may not be available when those setters are called, which results in runtime errors and loss of values. The use of modelProxy prevent those errors and preserves the values.

If i-components is build now, errors will be produced because SliderModel does not have a Minimum and Maximum properties. On Silverlight, those properties belong to RangeBase, so let's add them into RangeBaseModel:

That should be enough to store minimum and maximum values in the slider model, and to build i-components and the migrated application without issues. But the HTML input element on the slider component template will still behave the same, because it has not been modified to use the Minimum and Maximum properties from the model. Open the slider component template and add the following attributes:

That will set the input's min attribute to the slider component class minimum property, which gets its value from the Minimum property on the model. Similarly for max. That should set the correct range for the slider component (you can check min and max documentation on MDN). But there is still another issue: granularity.

On Silverlight, by default the Slider moves smoothly from one side to the other, and allows the user to select any real value between Maximum and Minimum (both included). However, the slider on HTML has a default granularity of 1, which means that by default the user can only select integers numbers between min and max. Given that minimum is -1 and maximum is 1 on the migrated application, the default HTML slider will only allow the user to select -1, 0 and 1. To change the default HTML slider granularity, the step attribute must be set:

Setting step to "any" should allow the user to select any real value between min and max, according to MDN documentation. Rebuild i-components, copy it into the migrated application and verify if the slider has the desired behavior:

Slider component, all to the right.

Hmmm, the slider is moved all the way to the right. When clicking on the Show button, it shows a Value of 0, so the model has the correct initial value. Using the browser development tools, select the <input> element inside <wm-slider> and check its value:

<input> value.

Aha! The initial value on the <input> element is "1", which is out of sync with the initial value on the model (0). After moving the slider, the value on the <input> is copied into the model, and both become synced, but initially they are different.

It turns out that the default value for a HTML slider is 50. When Angular changes the max attribute (because of the binding that was just defined) and sets it to 1, the initial value gets displaced to be inside the valid range, so it starts at 1, which is rendered all the way to the right.

This can be fixed by binding the value of the input element with the value on the model, so that they are synced from the beginning. Open the slider component class and add a property to expose the value from the model:

On the slider component template add a binding for the input value:

When the slider component is initialized by Angular, this binding will take the value from the model and set it on the <input> element. It will also update the value in the <input> element if the value from the model changes in code behind by any chance.

Rebuild i-components and copy it into the migrated application. Try the slider component again:

Slider component,

Good, the slider started in the correct position (in the middle). Now click on the Show button:

Slider component initial value.

Excellent! The initial value is correct. Now move the slider a little bit and click on the Show button again:

Slider component with modified value.

Great! The slider now behaves just as it did on Silverlight.

It is possible that, over time, more differences in behavior will be discovered between the Slider control on Silverlight and the slider component on i-components. This may happen because the control has different uses on different parts of the application, or because when running use cases those differences arise. In any way, once a functional component is available, those differences can be incrementally fixed as they come.

Resorces

Silverlight legacy application.
Original migrated application.
Patch to add support for slider into WebMAP Silverlight packages repository.
Files for slider model and component.
Modified migrated application, using the slider model and component from wms-framework and i-components.

Last updated

Was this helpful?