Quantcast
Channel: Develop 1 Limited Blog
Viewing all 296 articles
Browse latest View live

Deploying Webresources using spkl Task Runner


Continuous Integration using spkl Task Runner

Not all Business Process Flow entities are created equal

$
0
0

As you probably know by now, when you create Business Process Flows in 8.2+ you'll get a new custom entity that is used to store running instances (if not then read my post on the new Business Process Flow entities).

When your orgs are upgraded to 8.2 from a previous version then the business process flow entities will be created automatically for you during the upgrade. They are named according to the format:

new_BPF_<ProcessId>

Notice that the prefix is new_. This bothered me when I first saw it because if you create a Business Process Flow as part of a solution then the format will be:

<SolutionPrefix>_BPF_<ProcessId>

Here lies the problem. If you import a pre-8.2 solution into an 8.2 org, then the Business Process Flows will be prefixed with the solution prefix – but if the solution is in-place upgraded then they will be prefixed with new.

Why is this a problem?

Once you've upgraded the pre-8.2 org to 8.2 then the Business Process Flows will stay named as new_ and included in the solution. When you then import an update to the target org – the names will conflict with each other and you'll get the error:

"This process cannot be imported because it cannot be updated or does not have a unique name."

Source 8.1 Org
Solution with myprefix_

Empty 8.2 Org

Export

Import

 

BPF entity created - myprefix_BPF_xxx

Upgraded to 8.2

BPF entity created - new_BPF_xxx

 

Export

Import

 

"This process cannot be imported because it cannot be updated or does not have a unique name."

new_BPF_xxx conflicts with myprefix_BPF_xxx

 

How to solve

Unfortunately, there isn't an easy way out of this situation. There are two choices:

  1. If you have data in the target org that you want to keep – you'll need to recreate the BPFs in the source org so that they have the myprefix_ - you can do this by following the steps here - https://support.microsoft.com/en-us/help/4020021/after-updating-to-dynamics-365-mismatched-business-process-flow-entity
  2. If you are not worried about data in the target org you can delete those BPFs and re-import the solution exported from the upgraded 8.2 source org.

The good news is that this will only happen to those of you who have source and target orgs upgraded at different times – if you upgrade your DEV/UAT/PROD at the same time you'll get BPFs entities all prefixed with new_

@ScottDurow

Solution Packager and global optionset enum support in spkl Task Runner

$
0
0

I’ve published version 1.0.9 of spkl to NuGet - this adds the following new features:

  1. Global optionset enum generation for early bound classes.
  2. Solution Packager support

Global Optionset enum generation

This was a tricky one due to the CrmSvcUtil not making it easy to prevent multiple enums being output where a global optionset is used, but you can now add the following to your spkl.json early bound section to generate global optionset enums.

{
  "earlyboundtypes": [
    {
      ...
      "generateOptionsetEnums": true,
      ...
    }
  ]
}

In a future update, I’ll add the ability to filter out the enums to only those used.

Solution Packager Support

The solution packager allows you to manage your Dynamics metadata inside a Visual Studio project by extracting the solution into separate xml files. When you need to combine multiple updates from code comments, you can then use the packager to re-combine and import into Dynamics. To configure the solution packager task you can add the following to your spkl.json

 /*
  The solutions section defines a solution that can be extracted to individual xml files to make
  versioning of Dynamics metadata (entities, attributes etc) easier
  */
  "solutions": [
    {
      "profile": "default,debug",
      /*
      The unique name of the solution to extract, unpack, pack and import
      */
      "solution_uniquename": "spkltestsolution",
      /*
      The relative folder path to store the extracted solution metadata xml files
      */
      "packagepath": "package",
      /*
      Set to 'true' to increment the minor version number before importing from the xml files
      */
      "increment_on_import": false
    }
  ]

There are two .bat files provided that will call:

spkl unpack

This will extract the solution specifed in the spkl.json into the packagepath as multiple xml files

spkl import

This will re-pack the xml files and import into Dynamics - optionally increasing the version number of the solution to account for the new build.

Folders are back!

$
0
0

It's a long time since I've used the old SharePoint list component and for the most part, I've not missed it. Server to Server integration is slick and just works.

That said, the one thing that I do miss is support for folders - but whilst testing the new 9.0 Enterprise Edition I've noticed that folder support has been added in this latest release!

I was so excited I just had to share a little video of what it looks like

Fodlers are back

Maybe in the release after this, we'll get support for content types and metadata properties!

 

Counting Sheeps

$
0
0

One of the strangest part of the Dynamics CRM WebApi is the pluralisation of the entity names.

In the old OData endpoint, the entity set name was <EntityLogicalName>Set – however in the OData 4.0 endpoing, the Logical Name is pluralised by using a simplistic set of rules which often results in the incorrect plural name being picked.

This introduced a conundrum – Performance vs. correctness. Do we query the metadata for the Entity Set name at runtime – or use a duplicate set of over simplified rules in our JavaScript?

The New Version 9 Client Side API

The good news is that with version 9, the Xrm Api now supports:


Xrm.Utility.getEntitySetName("contact")

This will return "contacts" and so we can safely use this without worrying if the plural name is correct or not or indeed if it changes in the future.

Hope this helps!

 

 

executionContext hits the big time!

$
0
0

You've seen the executionContext in the event registration dialog and you might even have used it on occasion. Well with the release of Dynamic 365 Customer Engagement Version 9, it has been elevated to be the replacement for Xrm.Page.

The document describing the replacement for Xrm.Page details it as ExecutionContext.getFormContext – due to the capitalisation of ExecutionContext it implies that this is a global object, when in fact it must be passed as a parameter to the event handler by checking 'Pass execution context as first parameter' checkbox.

Oddly - It's still unchecked by default given its importance!

So why the change?

Imagine that we want to create a client side event on the Contact entity that picks up the parent account's telephone number and populates the contact's telephone when 'Use Account Phone' is set to Yes. We add the event code to both the form field on change and the editable grid on change for the 'Use Account Phone' field.

If we were to use the 'old' Xrm.Page.getAttribute method –it would work on the form but it wouldn't work within the grid on change event handler.

This is where the executionContext shines – it can provide a consistent way of getting to the current entity context irrespective of where the event is being fired from (form or grid).

Show me the code!

The following event handler is written using typescript – but it's essentially the same in JavaScript without the type declarations.

The important bit is that the executionContext is defined an argument to the event handler and attribute values are retrieved from the context returned by it's getFormContext() method.

static onUseCompanyPhoneOnChanged(executionContext: Xrm.Page.EventContext) {
    var formContext = executionContext.getFormContext();
    const company = formContext.data.entity.attributes.get<Xrm.Page.LookupAttribute>("parentcustomerid");
    const usePhone =
        formContext.data.entity.attributes.get<Xrm.Page.BooleanAttribute>(dev1_useaccounttelephone);
    const parentcustomeridValue = company.getValue();

    // If usePhone then set the phone from the parent customer
    if (usePhone.getValue() &&
        parentcustomeridValue != null &&
        parentcustomeridValue[0].entityType === "account") {
        const accountid = parentcustomeridValue[0].id;
        Xrm.WebApi.retrieveRecord("account", accountid, "?$select=telephone1")
            .then(result => {
                formContext.data.entity.attributes.get("telephone1").setValue(result["telephone1"]);
            });
    }
}

Some Additional Notes:

  1. All the attributes that are used in the event must be in the subgrid row (parent customer attribute in this case).
  2. You can access the parent form container attributes using
    parent.Xrm.Page.getAttribute("name").getValue()

Xrm.Page still works in Version 9 but it's a good idea to start thinking about giving executionContext the attention it deserves!

Hope this helps!

Script Load Dependencies in Version 9

$
0
0

A long time ago, Dynamics CRM introduced the concept of asynchronous loading of form web resources – this created a challenge when scripts depend on other scripts to be loaded first (e.g. inheritance or using a common type system library during script loading).

Version 9 has introduced an interesting feature where you can define the dependencies that a specific script has on other scripts.

Imagine you had 3 scripts

  • C.js requires B.js to load
  • B.js requires A.js to load

You can now define these dependencies in the web resources dialog:

I was hoping that by defining this dependency graph, the runtime would load them in the correct order like a module loader would – but having run some test the execution order still depending on the download speed and size of the script.

Script load execution order C - B - A

Script load execution order A - B - C

Conclusion

The Web resource dependency feature is awesome when you have many resources that are required during form events at runtime (e.g. onload, onchange etc.) You can simply add the single script into the form and the other dependencies will be loaded for you.

At this time, it's not a solution for where you need those dependencies during script load execution.


Ribbon Dependencies in Version 9 – isNaN is no more!

$
0
0

I recently blogged about the introduction of the script dependancies dialog in Version 9 where you can define the scripts that are needed by another. Although it does not solve the asynchronous loading issue for forms, it makes it simpler to add scripts to form since the dependencies will automatically be added for us.

Up until now, there has been a common pattern when adding script to Ribbon Commands where the dependancies were added with a function of 'isNaN'. It didn't have to be isNaN, but that is the most popular 'no operation' function call.

With the introduction of the script dependencies, you only need to include the reference to ClientCommands.js and the ClientCommon.js will be loaded automatically for you first before the command is called.

Awesome – we no longer need the isNaN approach that always felt like a 'hack'.

OAuth Server to Server Application User Authentication

$
0
0

Recently I've been getting asked a great deal about how to perform non-interactive authentication with the Dynamics 365 WebApi in a server to server authentication scenario. The most common scenario is that you have an external server application that needs to access the Dynamics 365 WebApi.

The good news is that it's easy using Application Users. Here is a short video showing you how.

https://www.youtube.com/watch?v=Td7Bk3IXJ9s

The code in the video is as follows:

public static async Task Auth()
{
    string api = "https://org.crm11.dynamics.com/api/data/v9.0";

    AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(
                new Uri(api)).Result;

    var creds = new ClientCredential("ApplicationID", "ClientSecret");

    AuthenticationContext authContext = new AuthenticationContext(ap.Authority);
    var token = authContext.AcquireTokenAsync(ap.Resource, creds).Result.AccessToken;

    using (HttpClient httpClient = new HttpClient())
    {
        httpClient.Timeout = new TimeSpan(0, 2, 0);
        httpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);

        HttpResponseMessage response = await httpClient.GetAsync(api + "/contacts?$top=1");
    }
}


 

Hope this helps!  

Version 9 Error - "Method not found: '!!0[] System.Array.Empty()'"

$
0
0

I recently found an issue with Dynamics 365 Version 9 where a call to ITracingService.Trace(message) from inside a plugin caused the following exception:

 

System.MissingMethodException: Method not found: '!!0[] System.Array.Empty()'

 

Or

 

System.MissingMethodException: Method not found:
'System.String System.String.Format(System.IFormatProvider, System.String, System.Object)'.

 

 

I simply needed to replace it with TracingService.Trace(message,null);

UPDATE: This issue is caused by compiling the Plugin using the 4.6.2 version of the .NET framework!

Hope this helps!

 

Adding Global Command Buttons to the Unified Interface

$
0
0

If you've used the new Version 9 Unified Interface so far then you'll know that there is no advanced find button yet. I am sure that it won't be long until this feature is added in some form and indeed much of the time the Relevance Search finds what you need quickly.

That said, I thought I'd demonstrate how to add command buttons to the top bar in the Unified Interface by using the example of adding an Advanced Find button similar to the Web Client.

What is the Global Command Bar?

The Unified Client has a similar looking top bar to the Web Client except it doesn't include the site map since it's moved to the left-hand navigation.

We can now add command buttons to this top bar, to the right of the + button.

How to add a Global Command Button

1. Create a new solution and add the Application Ribbon using

2. Add a new JavaScript web resource containing the following code:

var UCIAdvancedFind = {
    open: function(){
        var advFindUrl = Xrm.Utility.getGlobalContext().getClientUrl() + "/main.aspx?pagetype=advancedfind";
        Xrm.Navigation.openUrl(advFindUrl,{width:900,height:600})
    },
    isDesktop: function() {
        return Xrm.Utility.getGlobalContext().client.getFormFactor()==1;

    }
};

The isDesktop function is used on an EnableRule to ensure that the Advanced Find button doesn't show on the phone/tablet client because it will not work.

3. Add a new SVG icon to the solution to be used on the Global Command Button.

4. Load up the new solution in the Ribbon Workbench and locate the Mscrm.GlobalTab group in the Home Command Bar:

5. Drag a button into the Mscrm.GlobalTab isv area:

6. Create an Enable Rule that calls some custom JavaScript:

7. Create a command that is linked to the button that calls some JavaScript and has the Enable Rule:

Once this is published it'll show up in the top bar:

You can also add flyout menus to this bar. If there isn't enough room the Global Command Bar will show the overflow drop down menu like a flyout button.

Remember – this is only available on the UCI – which is awesome by the way!

You can download the managed solution for this button:

UCIAdvancedFind_1_0_managed.zip (3.60 kb)

Adding Ribbon Workbench SmartButtons to the Unified Client

$
0
0

If you've used or read about the Unified Client, there are some notable omissions from the command bar. This is mostly because the unified client is being developed and enhanced still and I suspect that the effort is being focused on stability and performance (both of which are very good already).

The most notable omissions are:

  1. Advanced Find – Advanced Find has not been implemented yet in the UCI – but in the meantime, you can add the old Web UI advanced find to the UCI using my solution that I've already posted about.
  2. Run Workflow – there is no run workflow button on forms or views at the moment. In this post, I'll show you how to add buttons to run specific workflows using Ribbon Workbench Smart Buttons
  3. Run Report – as for Run Workflow in this post I'll show you how to add a run report button.
  4. Run Dialog – I am not entirely sure that Dialogs will ever make their way to the Unified Client because they are deprecated.

What are Smart Buttons?

When you start the Ribbon Workbench, it scans for any smart button enabled solutions and displays them in your toolbox. Smart Buttons are essentially small pre-defined templates that can be added to your ribbon customisations and are defined by a smart button manifest in an installed solution. You can read more about them in my post explaining Smart Buttons in more detail.

Installing the 'starter' Smart Button Solution

Once you have installed the Ribbon Workbench, you can download and install my Smart Button Solution. You can obviously define your own smart buttons – but my solution provides some of the most common buttons that customisations need – namely Run Report and Run Workflow.

  1. Download the solution from https://github.com/scottdurow/RibbonWorkbench/releases
  2. Install the solution into Dynamics by importing it into the Dynamics Administration Solutions Area
  3. Create a solution that contains just the entity that you want to add smart buttons to
  4. Load the solution into the Ribbon Workbench

Running Workflows in the UCI

Once you have the smart buttons solution installed, you'll see them in the Ribbon Workbench toolbox:

Adding a Run Workflow and Run Report Button

You can simply drag the buttons from the smart button toolbox into the entity ribbons. You will be presented with a configuration box when you drop them onto the design surface:

The newly created buttons will appear on your design surface and also appear as buttons and commands in the Solution Elements

Ensuring that the Run Workflow and Run Report Buttons show in the Unified Client

The key to showing the smart buttons in the Unified Client is the Enable and Display Rules. Currently, the Smart Button templates are set up for the Web UI – in the future, I will adapt these for the Unified Client, but during this transitional period it is up to you to decide if you want them to appear or not by managing which rules are included.

To enable the 'Run Workflow' and 'Run Report' buttons you need to remove the display/enable rules from their respective commands:

  • Mscrm.HideOnModern
  • Mscrm.RunWorkflowPrimary
  • Mscrm.ReadReport

You do this by selecting the command in the solution elements panel, and then right-click 'Remove from Command'

You can then publish the solution and the buttons will appear in the UCI! You can also set some nice SVG images in the 'Modern Image' property of each button in the Ribbon Workbench by creating some SVG Webresources to use.

Probably the biggest difference when developing on the UCI is that you need to take a mobile/tablet-centric approach to your customisations. Whilst the Run Workflow button will work on the Mobile/Tablet App – the Advanced Find and the Run Report buttons use a user interface that was never designed for this type of use and will only work when using the UCI via a web browser.

Unified Client Form Properties ‘Cheat Sheet’

$
0
0

I've recently got back from talking about the Unified Client at D365UG in Dublin. It was an awesome week with lots of exciting information about PowerApps and the Common Data Model for Applications. Whilst partaking in the local hospitality (with a pint of Guinness) I was discussing the Dynamics 365 form designer with fellow MVP Leon Tribe and about how there are so many options available but with many of them being legacy and having no effect in the Unified Client. I promised to publish my 'cheat-sheet' of the options and their applicability, so here you are Leon!

Each of the properties are listed as being applicable to the Web UI Only, UCI Only, or for both.

 

Availability

Notes

Field Properties - Official Documentation on Field Properties

Composite Name

Web Only

 I'm not a fan of the composite fields - so I really don't miss them.

Composite Address

Web Only

 

Display Label on the form

Both

 

Field is read-only

Both

 

Turn off automatic resolution in field

Web Only

UCI doesn't auto-resolve fields when tabbing away

Disable most recently used items for this field

UCI Only

See notes below

Visible by default

Both

Fields can be shown/hidden using business rules/JavaScript

Available on phone

UCI Only

Web UI does not render on mobile

Default Lookup View

Both

Web - Default is only used in the popup dialog

Lookup View Selector Filtering

Both

Lookup view always included in filtered list in UCI

Lookup View Columns

Both

Web UI only shows 3 columns inline

UCI shows 2 columns until expand arrow selected

Display Search box in Lookup dialog

Web Only

UCI doesn't show lookup dialog

Inline View Search match highlighting

UCI Only

 See notes below

Only show records where...

Both

 

Tabs - Official Documentation on Tab Properties

Show the label of this tab on the Form

Web Only

Tabs always have labels on the UCI since they are really actual tabs!

Expand this tab by default

Web Only

Tabs do not collapse in the UCI

Visible by Default

Both

UCI and Web both allow dynamic show/hide of tabs using JavaScript

Tab Layout

Both

Tabs can have multiple columns in the UCI and Web UI

Tab Column Width

Both

Tab column width can be controlled in both the UCI and Web UI

Tab Columns

Both

 

Sections - Official Documentation on Section Properties

Section Columns

Both

Multi-Columned Sections will wrap in UCI with the space available. See below.

Available on Phone

UCI OnlyWeb UI is not rendered on the phone.

Field Label Alignment

Web Only

UCI - Labels are always centred

Field Label Position

Web Only

UCI - Labels will move to above the fields automatically when space is limited

Field Label Width

Web Only

Field Label width is a constant in the UCI

Show the label of this section on the Form

Both

 

Show a line at top of the section

No effect

V9 Web and UCI Sections have boxes around them

Sub Grid - Official Documentation on Sub Grid Properties

Display Search Box

Both

 

Display label on the Form

No effect

 

Panel header colour

Web Only

 

Show Chart Only

Web Only

 

Display Chart Selection

No effect

 

Display Index

Both

 

Automatically expand to use available space

Both

Sub-grids will expand in height until they reach the maximum records set and then start to page.

Header/Footer

Header Layout

Both

UCI will show a single row with overflow

Footer Layout

Both

UCI will show on a single line with overflow

 

Recently used records

The recently used records list in lookups are interesting since they are only shown in the UCI. They are especially useful for lookups like 'Regarding'.

Search Lookup Result Highlighting

The UCI has an awesome feature of highlighting your search term in the in-line lookup control. One of the advantages of the UCI is that it works across all devices and so does not show a pop-up dialog like the Web UI does.

Lookup Columns

In the Web UI, you can only see up to 3 columns in the in-line lookup control – where the UCI shows 2 until you click the expand arrow, where all the view columns are shown.

Tab & Section Columns

The UCI allows full control over the columns in a section, however, unlike the Web UI it is responsive and will wrap the fields and sections underneath each other.

Let's look at an example where we have a form design as shown below:

When resizing the UCI to various form factors, the tab will dynamically re-arrange it's self as follows:

Full width - all the columns and sections are shown as they are laid out in the form designer:

Narrower– wrapping section columns underneath each other:

Narrowest– wrapping sections underneath each other:

The Web UI will simply truncate the fields. This difference is one of the most important aspects of the UCI.

Header/Footers

The headers and footers in the UCI render very differently to the Web UI, where the fields that do not fit are added to the overflow flyout much the same as the Command Bar.

Header overflow flyout:

The fields that don't fit in the single header row are shown in the popup flyout:

 

Footer Overflow flyout:

The fields that don't fit into the footer single row are shown as a popup flyout.

Well, that's it. Overall the UCI honours most properties in the form designer where applicable and it does a great job of being responsive enough to show on both mobile and desktop form factors.

Model Driven Apps are GO!

$
0
0

The new Common Data Service for Apps promises to deliver the Xrm platform that we've been after for - well since forever! Today it was announced that it's no longer in Preview and has made it to General Availability (GA)!

Of course, with any GA announcement there is also a raft of licensing questions– the most important one for me was around how we can license the CDS for Application and build Model-Driven Apps without licensing for the Dynamics 365 1st Party Apps such as Sales & Servicing.

With the licensing documentation released today, Plan 2 appears to be that XRM type licensing, allowing building of Model Driven Apps (Dynamics Apps rather than Canvas Apps) with features such as real-time workflows and plugins.

The official license page shows a full breakdown of the license features, but below I've extracted the most important aspects with respect to building Model Driven Apps that we know as Dynamics 365 App Modules.

You can see that Plan 2 allows us to build Dynamics Apps without actually having a D365 license – with the exception of this new term 'restricted entities'. Conveniently, there is a list of which entities these are and the associated Dynamics 365 license required. The main take away is that you need a Customer Service license for full access to entities such as case/kbarticle/SLA and Project Service/Field Service license for the unified scheduling entities – with Plan 2 these restricted entities are read-only. This makes sense, as these entities are required by special functionality that is specific to those 1st party Apps that can only be accessed as part of a Dynamics 365 license.

It's worth noting that you can build Model Driven Apps with Plan 1 – but you can only use Async Workflows and Business Rules inside the CDS.

Why not learn more here:

Watch my video about how the Common Data Service for Apps is the new XRM Platform:


Let’s start TypeScript – Part 1

$
0
0

TypeScript isn't really a new language, it's a way of writing code in the next generation of JavaScript before it's fully supported by all browsers. By the time that browser support for ES6 is there, we'll be all writing TypeScript using ES9 features that 'transpile' down to ES6 code! Of course, there are cool features of the typescript compiler and related tools that are nothing to do with the JavaScript language standard, but you get the idea!

The key point is that TypeScript is a superset of JavaScript. JavaScript is TypeScript without all the new features and strict compiler checks. For this reason, TypeScript is nothing to be afraid of if you are already developing in JavaScript. You can convert your JavaScript into TypeScript with very little effort and then start to make use of the new features available in ES6 gradually over time.

This 2 part post is going to take the DependantOptionset.js JavaScript from the olds SDK samples and walk through converting it to TypeScript in the hope that it will help you see how easy it is to start using TypeScript!

Step 1 – Setup up your TypeScript project

I'm going to use Visual Studio 2017 because I like the fact that I can develop C# Plugins and TypeScript in the same IDE – but you could equally use VSCode.

  1. Download and install Node.js from https://nodejs.org/en/
  2. Select the 'Current' build and install.
  3. Open Visual Studio and install the 'Open Command Line' extension using Tools->Extensions and Updates…
  4. Search for 'Open Command Line' and select Download
  5. Restart Visual Studio to install the extension
  6. Select New -> Project
  7. Pick ASP.NET Empty Web Site
  8. Select Add->Add New Item…
  9. Select TypeScript config file
  10. Add the compileOnSave option:
    {
        "compilerOptions": {
            "noImplicitAny": false,
            "noEmitOnError": true,
            "removeComments": false,
            "sourceMap": true,
            "target": "es5"
        },
      "compileOnSave":  true,
      "exclude": [
        "node_modules",
        "wwwroot"
      ]
    }
  11. Add a folder WebResources with a subfolder js, and then place the SDK.DependentOptionSet.js file in that folder (you can pick up the file from the old SDK sample)
  12. I use spkl to deploy Web Resources, so use the NuGet Package Manager (Tools-> NuGet Package Manager -> Package Manager Console) to run
    Install-Package spkl
  13. Edit the spkl.json file to point to the SDK.DependentOptionSet.js file:
    {
      "webresources": [
        {
          "profile": "default,debug",
          "root": "Webresources/",
          "files": [
            {
                "uniquename": "sdk_/js/SDK.DependentOptionSet.js",
                "file": "js\\SDK.DependentOptionSet.js",
                "description": ""
            }
          ]
        }
      ]
    }
  14. Select the Web Site Project in the Solution Explorer and use the Alt-Space shortcut to open the command line
  15. Enter the following command on the command prompt that popups up:
    npm init
  16. Accept all the defaults
  17. You should now see a packages.json file in your project folder.
  18. To install the TypeScript definitions for working with the Dynamics client-side SDK, On the same console window enter:
    npm install @types/xrm --save-dev
  19. You will now have a node_modules folder in your project directory.

 

Step 2 – Updates to allow JavaScript to compile as TypeScript

Now you've got your project ready, we can start to use TypeScript.

  1. Copy the SDK.DependentOptionSet.js to a new folder named src and rename to be .ts rather than .js
  2. If you open the TypeScript file you'll start to see some errors that the TypeScript compiled has found since it is adding some stricter checks than ES5 JavaScript:

  3. Let's sort out the namespaces – in ES5 JavaScript, there was no concept of modules or namespaces so we had to manually contruct them – in this case we are creating a namespace of SDK with effectively a type called DependentOptionSet. We can convert this to the following TypeScript:
    namespace SDK {
        export class DependentOptionSet {
            static init(webResourceName) {
    Notice how the class is marked as 'export' so that it will be accessible to other TypeScript code, and the function pointer field init has be converted into a static method.
  4. We can repeat this for all the function pointer fields on the DependentOptionSet class and do the same for the Util class.

The outline will look like:

Step 3 – Attribute Strong Typing

  1. If you look at the methods now you'll start to notice that there are some more errors that the typescript compiled needs help with. The first we'll look at is there is an error on getValue(). This is because the typing for the attribute collection isn't what is expected.

  2. In order that the attribute variables are typed correctly, we change:
    Xrm.Page.data.entity.attributes.get(parentField)
    to
    Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(childField)
     
  3. The same should be repeated for the line:
    Xrm.Page.data.entity.attributes.get(parent)
  4. Repeat this for both ParentField and ChildField
  5. Next, we need to deal with TypeScript's type expectations:

  6. JavaScript allows fields to be defined on objects without any predefined typing, but TypeScript requires that we define the type at the point of assignment to the mapping variable.
    Consequently, the mapping type needs to be defined as follows:
    var mapping = {
        parent : ParentField.getAttribute("id"),
        dependent : SDK.Util.selectSingleNode(ParentField, "DependentField").getAttribute("id"),
        options : []
    };
    The same technique needs to be then repeated for the option and optionToShow variables.

Step 4 – Class level fields

The next issue that is highlighted by the compiler is that JavaScript allows assigning field level variables without prior definition.

  1. We must add the config field variable into the DependentOptionSet class definition:
    export class DependentOptionSet {
        static config = [];
  2. Now that there are no more compile errors, you should start to see the JavaScript generated:
 

Step 5 – Taking it one step further

You can now start to turn on stricter checks. The most common is adding to your tsconfig.json under compilerOptions:

"noImplicitAny": true,
  1. You'll now start to see errors in your TypeScript where there is no type inferable from the code:
  2. In this case we need to add some additional types for the Xml Http types that the browser use, so edit your tsconfig.json and the following to the compilerOptions:
    "lib": [ "dom","es5" ]
  3. You can now change the signature of completeInitialization to:
    static completeInitialization(xhr : XMLHttpRequest) {
  4. We'll also need to sort out other type inference issues such as:
  5. We can add the following type definitions
    namespace SDK {
        class Option {
            value: string;
            showOptions: string[]
        } 
    
        class Mapping {
            parent: string;
            dependent: string;
            options: Option[]
        }
  6. Finally, the code should look like:
namespace SDK {
    class Option {
        value: number;
        text?: string;
        showOptions?: Xrm.OptionSetValue[]
    } 

    class Mapping {
        parent: string;
        dependent: string;
        options: Option[]
    }
 

    export class DependentOptionSet {
        static config: Mapping[] = [];
        static init(webResourceName : string) {
            //Retrieve the XML Web Resource specified by the parameter passed
            var clientURL = Xrm.Page.context.getClientUrl();
 

            var pathToWR = clientURL + "/WebResources/" + webResourceName;
            var xhr = new XMLHttpRequest();
            xhr.open("GET", pathToWR, true);
            xhr.setRequestHeader("Content-Type", "text/xml");
            xhr.onreadystatechange = function () { SDK.DependentOptionSet.completeInitialization(xhr); };
            xhr.send();
        }
 

        static completeInitialization(xhr : XMLHttpRequest) {
            if (xhr.readyState == 4 /* complete */) {
                if (xhr.status == 200) {
                    xhr.onreadystatechange = null; //avoids memory leaks
                    var JSConfig: Mapping[] = [];
                    var ParentFields = xhr.responseXML.documentElement.getElementsByTagName("ParentField");
                    for (var i = 0; i < ParentFields.length; i++) {
                        var ParentField = ParentFields[i];
                        var mapping : Mapping = {
                            parent : ParentField.getAttribute("id"),
                            dependent : SDK.Util.selectSingleNode(ParentField, "DependentField").getAttribute("id"),
                            options : []
                        }; 

                        var options = SDK.Util.selectNodes(ParentField, "Option");
                        for (var a = 0; a < options.length; a++) {
                            var option : Option = {
                                value: parseInt(options[a].getAttribute("value")),
                                showOptions: [],
                            };
                            var optionsToShow = SDK.Util.selectNodes(options[a], "ShowOption");
                            for (var b = 0; b < optionsToShow.length; b++) {
                                var optionToShow : Xrm.OptionSetValue = {
                                    value: parseInt(optionsToShow[b].getAttribute("value")),
                                    text: optionsToShow[b].getAttribute("label")
                                };
                                option.showOptions.push(optionToShow)
                            }
                            mapping.options.push(option);
                        }
                        JSConfig.push(mapping);
                    }
                    //Attach the configuration object to DependentOptionSet
                    //so it will be available for the OnChange events 
                    SDK.DependentOptionSet.config = JSConfig;
                    //Fire the onchange event for the mapped optionset fields
                    // so that the dependent fields are filtered for the current values.
                    for (var depOptionSet in SDK.DependentOptionSet.config) {
                        var parent = SDK.DependentOptionSet.config[depOptionSet].parent;
                        Xrm.Page.getAttribute(parent).fireOnChange();
                    }
                }
            }
        } 

        // This is the function set on the onchange event for 
        // parent fields
        static filterDependentField(parentField: string, childField : string) {
            for (var depOptionSet in SDK.DependentOptionSet.config) {
                var DependentOptionSet = SDK.DependentOptionSet.config[depOptionSet];
                /* Match the parameters to the correct dependent optionset mapping*/
                if ((DependentOptionSet.parent == parentField) && (DependentOptionSet.dependent == childField)) {
                    /* Get references to the related fields*/
                    var ParentField = Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(parentField);
                    var ChildField = Xrm.Page.getAttribute<Xrm.Attributes.OptionSetAttribute>(childField);
                    /* Capture the current value of the child field*/
                    var CurrentChildFieldValue = ChildField.getValue();
                    /* If the parent field is null the Child field can be set to null */
                    if (ParentField.getValue() == null) {
                        ChildField.setValue(null);
                        ChildField.setSubmitMode("always");
                        ChildField.fireOnChange(); 

                        // Any attribute may have any number of controls
                        // So disable each instance
                        var controls = ChildField.controls.get()
 

                        for (var ctrl in controls) {
                            controls[ctrl].setDisabled(true);
                        }
                        return;
                    }
 

                    for (var os in DependentOptionSet.options) {
                        var Options = DependentOptionSet.options[os];
                        var optionsToShow = Options.showOptions;
                        /* Find the Options that corresponds to the value of the parent field. */
                        if (ParentField.getValue() == Options.value) {
                            var controls = ChildField.controls.get();
                            /*Enable the field and set the options*/
                            for (var ctrl in controls) {
                                controls[ctrl].setDisabled(false);
                                controls[ctrl].clearOptions();
 

                                for (var option in optionsToShow) {
                                    controls[ctrl].addOption(optionsToShow[option]);
                                }
 

                            }
                            /*Check whether the current value is valid*/
                            var bCurrentValueIsValid = false;
                            var ChildFieldOptions = optionsToShow;
 

                            for (var validOptionIndex in ChildFieldOptions) {
                                var OptionDataValue = ChildFieldOptions[validOptionIndex].value;
 

                                if (CurrentChildFieldValue == OptionDataValue) {
                                    bCurrentValueIsValid = true;
                                    break;
                                }
                            }
                            /*
                            If the value is valid, set it.
                            If not, set the child field to null
                            */
                            if (bCurrentValueIsValid) {
                                ChildField.setValue(CurrentChildFieldValue);
                            }
                            else {
                                ChildField.setValue(null);
                            }
                            ChildField.setSubmitMode("always");
                            ChildField.fireOnChange();
                            break;
                        }
                    }
                }
            }
        }
    }
 

    export class Util {
        //Helper methods to merge differences between browsers for this sample
        static selectSingleNode(node: Element, elementName : string) {
            if ((<any>node).selectSingleNode) {
                return <Element>(<any>node).selectSingleNode(elementName);
            }
            else {
                return node.getElementsByTagName(elementName)[0];
            }
        } 

        static selectNodes(node: Element, elementName : string) {
            if ((<any>node).selectNodes) {
                return <NodeListOf<Element>>(<any>node).selectNodes(elementName);
            }
            else {
                return node.getElementsByTagName(elementName);
            }
        }
    } 

}

The differences between the original JavaScript and the TypeScript are mostly structural with some additional strongly typing:

The resulting differences between the original JavaScript and the final compiled TypeScript are also minimal:

Of course, the key difference is now that we have strong type checking through type inference. Type inference is your friend – the compiler knows best!

In the next part, I'll show you how to deploy and debug this TypeScript using source maps.

 

Let’s start TypeScript – Part 2

$
0
0

In the first part of this series, we looked at the simple steps to convert the SDK Sample Dependant OptionSets into TypeScript so you can see that it's not about 're-writing' your code. So far, we've seen how to get the TypeScript compiler to compile our old JavaScript. This part is going to look at deploying and debugging the TypeScript code.

Deploying the webresources

  1. For the DependantOptionSet code to work we need to also create a config file that defines which child optionsets are filtered by which parent values, so create an xml file in the js folder called AccountOptionSetConfig.xml:
    <DependentOptionSetConfig entity="account" ><ParentField id="address1_shippingmethodcode"
                  label="Shipping method"><DependentField id="address1_freighttermscode"
                      label="Freight Terms" /><Option value="2"
              label="DHL"><ShowOption value="1"
                   label="FOB" /><ShowOption value="2"
                   label="No Charge" /></Option><Option value="3"
              label="FedEx"><ShowOption value="1"
                   label="FOB" /></Option></ParentField></DependentOptionSetConfig>
    You solution should look like the following:

  2. Next we need to enable deploying the webresources. In the previous part we installed spkl and added the DependentOptionSet webresources. If you've not done this, I suggest you go and review Part 1.
  3. Adding the config xml with spkl is really easy, simply add the new file to the spkl.json file:
    {
    "uniquename": "sdk_/js/AccountOptionSetConfig.xml",
    "file": "js\\AccountOptionSetConfig.xml",
    "description": ""
    }
  4. You can now deploy by running the spkl\deploy-webresources.bat at the command-line.
    Since we installed 'Open Command Line' in the previous part, we can simply select the spkl folder in Visual Studio and press Alt-Space to open the command prompt!

Testing inside Dynamics

Now that we've deployed the webresources, we can configure it for use on the Account form.

  1. Open the Account Form inside Dynamics Form designer. I find this easiest to do from your App in the designer:
  2. Select Form Properties and add the DependentOptionSet.js to the Form Libraries:
  3. Add a new OnLoad Event Handler that calls the SDK.DependentOptionSet.init function with the Parameters (including the quotes) "sdk_/js/AccountOptionSetConfig.xml" 
  4. Locate the Shipping Method field on the form, and add a new On Change handler calling SDK.DependentOptionSet.filterDependentField with the parameters "address1_shippingmethodcode","address1_freighttermscode"
  5. These steps are simply necessary to wire up the code to the form events. If you save the form and publish you should now see the Shipping Method drop down filter the Freight Terms based on the configuration xml.
     

Debugging TypeScript with Fiddler

It is inevitable that whilst you are refactoring your JavaScript into TypeScript you will need to debug your code. This will involve making changes and re-testing. Deploying the code files up to the Dynamics server with each change in order that we can test will take up valuable time. Instead, whilst we developer we can use a 'trick' that uses Fiddler to redirect the requested webresources to the local version of the file instead of the version on the server. This allows us to make changes and simply refresh the browser to get the new version. I blogged about this trick back in 2014, but here is are the steps to set it up:

  1. Install Fiddler2 from https://www.telerik.com/download/fiddler Run Fiddler.Select Tools->Options->HTTPS and select Decrypt HTTPS traffic.
  2. Click Yes when prompted to Trust the Fiddler Root certificate, and then Yes to each subsequent dialog.
  3. Click OK on the Options Dialog. This allows Fiddler to decrypt the HTTPS traffic between the browser and the server so that it can intercept it, log and respond with a different file where needed.
  4. In Fiddler select Rules->Performance->Disable Caching. This will ensure that files are not stored locally in the browser cache, but downloaded with each page refresh to pick up the latest version
  5. In Fiddler Select the AutoResponder tab and select 'Enable rules' and 'Unmatched requests passthrough'
  6. Click Add Rule and enter:
    REGEX:(?insx).+\/sdk_\/js\/(?'fname'[^?]*.js)
    C:\Users\Administrator\source\repos\StartUsingTypeScript\StartUsingTypeScript\src\${fname}

    Note: Adjust the path to your own project location

  7. Click Save
  8. You should see a grey highlighted request for the files that match – which is now redirecting to your local file.
  9. When you refresh your page, you can now make changes to your local TypeScript which will recompile to a local js file and picked up without a re-deploy and re-publish.

Debugging TypeScript with Source Maps

Since we are no longer writing JavaScript directly, we need a way of stepping through our TypeScript code when debugging rather than the generated code. Luckily most debuggers have support for source maps that provide an index between the original source and the generated source. To enable this, we'll need to let the browser know where to load the TypeScript that is referenced by the .map files. The easiest way to do this is with another Fiddler autoresponder rules:

  1. In Fiddler AutoResponders, use Add Rule
  2. Set the Rule to be:
    REGEX:(?insx).+\/sdk_\/js\/C:\/(?'fname'[^?]*)
    C:/${fname}
  3. This now will pick up the locations in the source map and redirect to your local folder.
    https://org.crm11.dynamics.com/%7b636672751920000751%7d/webresources/sdk_/js/C:/Users/Administrator/source/repos/StartUsingTypeScript/src/SDK.DependentOptionSet.ts

    is redirected to

    C:/Users/Administrator/source/repos/StartUsingTypeScript/src/SDK.DependentOptionSet.ts

  4. You can now put a debugger statement in your TypeScript and you'll see the browser step through the original TypeScript code rather than the actual JavaScript.

 

So there you have it – you have all the same features as you did when writing JavaScript - but with all the advantages of TypeScript.

In the next part we'll look at how we can use gulp to create a minified version of the JavaScript for deployment.

My favourite PowerApps/Dynamics CE features in the October 2018 Release

$
0
0

Yes it's that time of the year again when we get to see the new features planned in the next major release of Dynamics 365 CE/PowerApps. I've already said how excited I about the changes to the release strategy announced earlier this month and now we can see what's on the roadmap.

There are over 250 pages of content spanning all the Dynamics 365 products, but here are my top 5 features that I'll be blogging about in the coming months:

  1. Dynamics 365 AI for Sales app
    We've all been there where someone asks for 'AI' but what they really need is an if/then statement - but this feature promises to offer natural language processing Q&A and predictive lead/opportunity scoring. Can't wait to see what new sales scenarios this will support. The great thing about this technology is that it will be ready to use rather than needing expensive development work using services such as Azure Cognitive Services.
  2. Increased Unified Client coverage
    We are going to see a tonne of new features added in the Unified Client, closing the gap between the 'classic' web UI. Notably, Advanced Find, Run Workflow, Grid Filtering, Service Administration
  3. Dependent optionsets in the Unified Interface
    Proving the point that new features will come to the UCI and not the 'classic' web UI - we now have the ability to create dependent option sets in the UCI.
  4. Custom controls in Business Process Flows
    The Custom Control Framework promises to be amazing, but this enhancement opens up many new scenarios in the Unified Client
  5. Faster Model Driven Apps!
    Performance improvements have been traditionally overlooked in new releases - but the already snappy UCI will be even faster and reliable!
  6. SharePoint Documents available on Portals
    This has been an ask for a long time and now we can surface documents stored in SharePoint through the Dynamics integration already available today.
  7. admin.powerapps.com get's solution support - no more 'classic' solution explorer!
    Whilst the default CDS Solution will still be there - we can now use solutions to manage our CDS configuration, exporting from one environment to another. This is massive because the new solution management experience includes a new WSYSIWYG form designer and so much quicker than the 'classic' solution explorer.
  8. Embedding Canvas Apps and Flows in solutions
    Application Lifecycle management with Model Driven Apps is already mature with it's use of solutions, where Canvas Apps and Flow has been lagging behind. Now we will see the ability to include these along side Model Driven Apps in solutions, and export from one environment to another. The ability to run flows and embed Canvas Apps inside a Model Driven App now means that we can start to really use these features, safe in the knowledge that they can be deployed to UAT/PROD from DEV.
  9. Unified experience for Flow, PowerApps and CDS
    These three complimentary products will be unified so that they are managed in the same experience. This can't come soon enough because I've found many people confused about how they fit together.
  10. Solution Checker
    Lastly, this new feature is interesting and offers advice on use of legacy features that will be deprecated in the future.

Go check out the October 2018 release notes now - https://aka.ms/businessappsreleasenotes 

Delegable or not Delegable, that is the query!

$
0
0

Silly title I know – couldn't help myself! The significance of this topic is certainly not silly - it could give your app trouble free scalability or … well not.

Delegable queries in Canvas Apps

A delegable query is simply a query that offloads the heavy lifting to the connector source rather than downloading all the data and doing the processing locally in your Canvas App.

A good example is when using the filter command. You can associate a gallery to the Accounts entity via the command:

Filter(Accounts, AccountSearchText.Text in name)

This will result in a fetchxml query condition sent to CDS of:

<filter type="and"><condition attribute="name" operator="like" value="%contoso%" /></filter>

You can also use:

Filter(Accounts, StartsWith(name,AccountSearchText.Text))

Which will give the fetchxml filter:

<filter type="and"><condition attribute="name" operator="like" value="Contoso%" /></filter>

All these queries are delegable and are the most optimal for large datasets.

Consider however, if you use a non-delegable predicate such as:

Filter(Accounts, Len(name)>5)

This will result in the yellow-triangle-of-doom in the Canvas App designer with a tool tip saying:

"Delegation warning. The highlighted part of this formula might not work correctly with column "name" on large data sets…"

This is because Len is not in the list of supported delegable predicates – you can see the complete list in docs.microsoft.com (I've submitted a pull request to update this list because CDS and Dynamics 365 connectors actually have much better support than when first released)

Optimising for large queries

If you do need to perform one of these non-delegable predicates in your filter, you can also combine the filter with an initial query that pulls down a smaller subset using a predicate that is supported. To do this you need to nest the filters:

Filter(
    Filter(Accounts, AccountSearchText.Text in name),
    Len(name)>5
)

Delegable Queries in Flow

If you browse the Dynamics 365 templates available for Flow, you'll see a flow called 'Add notes to a Dynamics CRM contact'

This flow basically allows you to search for a contact via a flow button and add a note to the matching contacts. The flow is simply:

  • Query Contacts
  • For each contact
    • If the first name and last name match those entered when triggering the flow
      • Create a note

Seems straightforward until you consider delegable queries. If you have thousands of records, flow will attempt to download them all and loop through them one by one since the conditions are not delegable to the initial query.

You'll see the effect of this if you test the flow and look at the number of records in the Apply to each. There are 29 records in this CDS instance and all are returned by the query since the query isn't delegated.

This solution is definitely not scalable, and I'm surprised it's in the list of templates offered! To make the query delegable, the connector must be edited to include an OData query:

This query will then scale as the number of contacts grows in your database because the heavy lifting of the query is delegated to the CDS database. These kind of performance considerations are important when building apps that will scale with the data-source. PowerApps makes it super easy to build amazing user experiences, but we sure to keep an eye on the App Checker ( ) since it's full of suggestions that'll keep your app running smoothly.

Further reading:

PowerFlappy - Canvas App Power Apps for c# and JavaScript Developers

$
0
0

As promised, I'm posting the code up for PowerFlappy! This post aims to give c# and JavaScript developers some tips on creating Canvas Apps.

To install, simply open web.powerapps.com, select Create an App, then select Open. It works really well on your mobile. I hope to see someone playing it on the tube on Monday!

Canvas Apps are function driven. Very much like Excel, they operate on values and tables of values using functions that accept these values as parameters.

You can bind controls to data so that it will change as the underlying data changes. It is like the data-binding principle you get with KnockoutJS and Angular etc.

Here are some common patterns that you'll need to use if you are writing Canvas Apps if you are a c# developer.

…semicolons and new lines

If you are a c# or JavaScript developer, then you are used to semicolons. It's practically subconscious; Canvas Apps follows suit but with one exception – the last line of a code segment!

This is rather troublesome when you add more lines because you'll need to remember to put a semicolon on the previous line before starting. To get around this by always adding 'true' to the end of a code segment:

This way, you can add new lines above 'true' and you'll not have to worry about not adding semicolons for the last line.

Oh, and you are going to find quickly that just like in Excel you need to use SHIFT-RETURN to get a new line – just like in Excel!

Variable

You can set global variables in any function. They don't have to be defined before hand - simply use:

Set(myVariable, "FooBar")

Collections

It's easy to add array style data using a JSON style format:

// Initialise a new collection
ClearCollect(collection, {i:1, x:100,y:100},{i:2, x:200,y:100},{i:3,x:300,y:100},{i:4,x:300,y:100});
// Add a new item to the collection
Collect(collection, {i:5,x:100});

Timers

Timers are a great way of making your code run at a specific interval. In a game, this would be typically be the main game loop. Since it's easy to have code spread all over your PowerApp – I find it useful to keep the majority of my code in one place.

Set your timer as follows:

You can then add the code to the Timer OnStartTimer event which will run every 100 milliseconds. Put it in the OnStart rather than the OnEnd to make it run immediately.

Think in Functions

If you had a collection of values, and you wanted to take an action on each of them based on their value, in c# you would commonly use something like:

foreach (var item in collection) {
item.x = item.x - 10;
if (item.x < 0) {

        item.x = item.x + 1000;
        }
} 

In Canvas Apps you can't do this kind of sequential logic, you have to think in functions like you would in a cell of Excel:

UpdateIf(collection, true, {x:x-10}); // Update all items

UpdateIf(collection, x<0, {x:x+1000}); // Update only those items where x<0 


This will filter the items that match the criteria and then update them accordingly. It kind of turns the foreach loop inside out!

Indexers

If you have a collection of items, it's quite normal to want to retrieve a value at a specific index. In C#:

var item = collection[4];

In Canvas Apps:

Set(item, Last(FirstN(collection,4)))

This clearly isn't optimal for large collections, so I hope there is a better option and will report if I find one. Canvas Apps compile into JavaScript - and collections are stored as simple object arrays with the Last and FirstN functions just operating on those array. You could use the Lookup function, but this ends up taking loner since it uses a delegate function to find the value rather than a simple array index.

Sprites

Canvas Apps has the concept of Galleries that allows you to create a template that is repeated for each data row. You might think that you could use this to create lots of sprites to use in your game – but unfortunately, the gallery control is limited in that you cannot move the controls outside of bounds of the screen and it's actually very slow at rendering.

To overcome this I used a set of 'generic' images that I then bound to a sprite collection. This allows me to set the values in the collections to whatever I need and the sprites will move and change size/image as required due to the data binding.

To create the side scroller, I simply scroll the sprites left using an UpdateIf as describe above. When it comes to graphics performance - it's a good idea to keep things a simple as possible.

Level Data

A trick that I've been using for years is to use Excel to write my code for me. If there is some data that you can use to generate code, then why write it manually?

For the levels, I created a simple Excel Spreadsheet that visualises the levels – and then created the collection data in the format {x:<top ground position>,y:<lower ground position>,l:<level up indicator>}

I guess I could create a level designer in a PowerApp too!

Performance

One of the challenges I had was keeping the scrolling smooth for the sprites. There were two things I found key to this:

  1. Only move sprites in increments of whole pixels. If you start to set sprites using fractions of pixels the rounding effect will create jitter
  2. Keep the timer interval higher than the time it takes to process each tick event.

Overall Maintainability

A trade off you get with Canvas Apps for the 'low-code' is that you don't get the easy to follow code files in traditional programming. Here are some tips to keep your App maintainable:

  1. Avoid having lots of code squirrelled away in many different events and property functions. For example, rather than having lots of timer events I have just a single game loop that does 90% of the functionality.
  2. Avoid functions in control property bindings, bind to a variable and set it to the value you need along with any calculations and conditions.
  3. Name your controls and variables consistently so that you can quickly identify what you need when using intellisense.

Well, there you have it. Happy Power Flapping - be sure to tweet me your highest score @ScottDurow.

Viewing all 296 articles
Browse latest View live


Latest Images