Model-Driven Development with Xomega.Net
Comprehensive step-by-step guide to the Xomega.Net development process
The best way to understand the capabilities of our Model-Driven Development platform, and appreciate the speed of the application development it provides, is to walk through a process of building a full fledged end-to-end application from scratch. In this tutorial we will show you how to build rich web and desktop applications from a sample Microsoft database AdventureWorks. This database covers many facets of an enterprise information system for a mid-size company from HR and procurement to production and sales. We will show you how to build a sample application for the Sales module on top of that database.
You will see how Xomega.Net helps you get off the ground quickly with its solution and project templates that are preconfigured using best practices for your selected architecture, and then lets you model your application and tailor it to your specific requirements. From there you will generate most of the application code and artifacts right from the model, and then will add security, and any necessary customizations for the generated code. The following table outlines the different steps and use cases that we cover in this tutorial.
Before you start
1. Generating basic search and details views
1.1 Creating ASP.NET Xomega solution
1.2 Importing Xomega model from a database
1.3 Adding CRUD operations and views
1.4 Building the model
1.5 Running the application
2. Modeling the search view
2.1 Modeling the results list
2.2 Adjusting fields’ visibility and labels, configuring links’ display
2.3 Implementing custom result fields
2.4 Modeling the search criteria
2.5 Defining static enumerations
2.6 Defining dynamic enumerations
2.7 Using auto-complete with enumerations
2.8 Configuring cascading selection
2.9 Adding custom UI validations
3. Modeling the details view
3.1 Updating details child list
3.2 Handling read-only fields
3.3 Grouping fields as child data objects
3.4 Using a multi-value property for a simple sub-object
3.5 Showing fields from related objects
3.6 Building a lookup form for selection
3.7 Adding contextual selection
3.8 View layout and master-details view
4. Implementing security
4.1 Creating claims identity
4.2 Adding OWIN authentication
4.3 Securing business services
4.4 Securing UI views
5. Adding a WPF desktop client
5.1 Adding client-server WPF project
5.2 Securing client-server application
5.3 Adding WCF middle tier
5.4 Securing WCF services
6. Adding a SPA web client
6.1 Adding REST services
6.2 Adding SPA client project
6.3 Securing SPA views
6.4 Using auto-complete in SPA
6.5 Configuring cascading selection in SPA
6.6 Custom UI validations in SPA
6.7 Adding contextual selection in SPA
Next steps
Before you start
To run this tutorial you will need to make sure you have the following installed on your machine:
-
Visual Studio 2015 Community Edition or other editions.
-
The latest Visual Studio updates (i.e. Update 3, with Nuget Package Manager ver. 3.4.4)
-
Xomega.Net plug-in for your version of Visual Studio.
-
Valid Xomega license.
-
Sample Microsoft database
AdventureWorks
installed on your local or network SQL server. This tutorial is based on version 2012 of this database.
-
TypeScript
for your version of Visual Studio to build a SPA web client.
The complete final code for this tutorial is also available on
GitHub
, so you can always download and run it, and use it as a reference when walking through the steps.
Without further ado, let's get started by creating a solution for our application projects, and generating some basic search and details views out of the box.
1.
Generating basic search and details views
As a first step we will create a new ASP.NET solution pre-configured for Xomega architecture and modeling platform. After that we will import our model from the sample database, add CRUD operations, as well as search and details views based on these operations, to the model, and finally we’ll generate end-to-end application code for our services and views.
1.1 Creating ASP.NET Xomega solution
To create a new Xomega solution select the New Project option in your Visual Studio and pick Xomega node in your installed templates. You will see various Xomega solution templates listed as follows.
Pick the Xomega ASP.NET Web Solution template, set AdventureWorks as the solution name, select your location and click OK.
Creating the solution may take a while as all the dependency packages are being copied, and the projects are being configured, but once the dust settles, you will see the following five projects created in your solution.
-
AdventureWorks.Model
is the project that will contain your XML domain and service model for the application, and various generators that perform model transformations and code generation.
-
AdventureWorks.Services.Common
project will have all interfaces and data contracts for your service model, as well as other classes that are shared between the client and the services layers.
-
AdventureWorks.Services.Entities
project will contain the back-end domain classes based on the Entity Framework, as well as implementations of the services that use these domain classes.
-
AdventureWorks.Client.Common
project will contain Xomega Framework data objects for the presentation layer, as well as view models for different views, and will encapsulate a significant part of the client logic. These objects and view models are not specific to the web client, and can be also used by other clients, such as WPF desktop clients, which would allow making their client logic reusable. This is why they are configured to have their own project.
-
AdventureWorks.Client.Web
project is the main web application project that has ASP.NET views.
Now that you understand the solution structure, let’s import our initial Xomega model from the sample database.
1.2 Importing Xomega model from a database
First thing you need to do in order to import Xomega model from a database is to set up the connection properties for the corresponding generator. Expand the Generators node in the AdventureWorks.Model project, and find the "Import from Database" generator under the Model Enhancement folder, which groups generators that enhance the model with additional information. Double-click on the generator to open its properties, and click on the editor for the Database Connection property, which will pop up a Database Connection Configuration dialog.
You will need to provide the connection string, which you can also specify in a separate dialog by clicking the Configure button. We will select the SQL Native Client provider, and the sample AdventureWorks database on the local SQL server instance using windows authentication, as shown below.
Once you setup the connection string and click Next, you will be prompted to exclude any tables from the import, but you can skip that to import all tables. Next, verify the database information, select the option to save it as a project default configuration, and then click Finish.
Finally, right click on the Import from Database generator, and select the Generate option from the context menu.
Once the generator is finished running, you should see the imported model files under the Import folder, which is specified in the generator's Output Path property by default. The imported files are organized to have each object in its own file, which roughly maps to the database tables, and grouped by module, which is derived from the database schema of the table. Here's how it will look like.
The model objects were imported from the database using the database structure, as well as any model setup that existed before the import, such as the logical types defined. This is a good start, but it is not enough to build a full-fledged application beyond just a “database table editor”. To model our application and describe different services and views that it will have, we need to further enhance our model with all that information, which is what we're going to do next.
1.3 Adding CRUD operations and views
When you are building a real application, you will pretty much know what kind of views you want and which objects they will be based on. But, for the purposes of this tutorial, let's pick “sales order” object to build a list screen for searching sales orders, and a details screen for creating new and editing existing sales orders. This is a reasonably large and complex object to demonstrate different features of the Xomega platform, which can also realistically represent the sales module of our application.
Let's go ahead and open up the imported sales_order_header.xom file, which contains the XML for our Xomega object model. To make reading and browsing this XML much easier, Xomega allows you to collapse it to definitions of different model entities. So go ahead and select the Outlining > Collapse To Definitions in the context menu for that XML to see it better, or just press the Ctrl+M, Ctrl+O shortcut.
Based on the special relationships (with cascading delete) between the SalesOrderHeader table and its child tables (SalesOrderDetails and SalesReason), the latter have been imported as subobjects of the sales order object, and automatically include its keys implicitly. The following picture demonstrates this.
However, the objects' names (highlighted) have been derived from the corresponding tables' names, and may not provide particularly good choices for other entities that will be generated from them. This is especially true for subobjects, since Xomega generators always try to fully qualify them with their parent object’s name.
Therefore, before we move any further, let's rename these objects to make them more succinct, and to work better with the Xomega generators as follows. We will rename "sales order header" to just "sales order" (including the file name), "sales order detail" to "detail", and "sales reason" to "reason". It is important to
do it before
we add any standard data objects or views to avoid tedious renaming afterwards.
While we're at it, we can also rename some long type names, such as the ones shown below, but it's not that critical, since we can do it at any time later.
Xomega provides standard refactoring capabilities, were you can right click on a type, and pick Rename from the context menu. Along with the Rename dialog, where you supply the new name, the system will also display all references to the current type, which will be renamed as well, as shown in the following picture.
Once all objects are properly renamed, all you have to do is to right-click on the sales_order.xom file and run the Full CRUD with Views generator under the Model Enhancement submenu. This is a configurable and powerful generator that can add various model elements you specify to one or more selected objects.
Here is a high-level view of the different things it added.
As you can see, it added standard create, read, update, delete (CRUD) and read list operations to the objects, defined Xomega data objects for view models, as well as the actual search and details views for the objects and subobjects in the file.
Under the hood it also configured each of these elements properly, so that views are bound to the data objects, and the data objects are tied to the operations, set up as each other children as needed, and have proper links to other views.
1.4 Building the model
Now that we have a domain model and a basic service model with views for the sales order object, we need to run all rerunnable generators to generate the actual code for our application. All such generators have
Include in Build
property set to
True
by default (which you can configure as you like), so essentially we need to build the model project to run these generators.
Note that the
model
project will
not
be
built automatically
when you build the entire solution, since it needs to be built only if the model changes. Therefore, in order to build the model you have to manually run the build for the model project by right-clicking on the project and selecting the Build menu option.
As part of building Entity Data Model, the generator will run standard Entity Framework T4 text templates, so you may get the following security warning.
You can check “Do not show this message again” to prevent it during subsequent builds, but you can also control this through the Tools > Options menu under the Text Templating section.
The output console for the build will print the generators being run and any warnings or other output from the generators as shown below.
You can resolve the warning about the “numeric” logical type later by updating the type that inherits from it.
The warnings from the Entity Data Model (EDM) generator tell you that some fields in the database and the model use a hierarchy type that is not supported by the Entity Framework yet, and therefore will be skipped in the generated EDM.
As you can see, the model build process, among other artifacts, also generated service implementations and ASP.NET views. This allows us to go straight to building the entire solution and running the application.
1.5 Running the application
Let’s go ahead and build the solution. If everything has been set up and run correctly, you should get no build errors. In order to debug the application we need to set the AdventureWorks.Client.Web project as a start-up project for the solution. However, rebuilding the model project will reload other
project
s in the solution, and may thereby reset the startup project. To prevent this from happening, we will open the solution properties, and set our startup project under the Multiple startup projects option as follows.
After clicking OK, hit F5 to start the application in the Debug mode. Your browser should launch the home screen of the web application with a top level menu. The menu will have a sub-menu for our Sales module, which contains menu options to open the search form for sales orders, and a form to create new sales orders.
Let’s click on the Sales Order List menu to see the generated search form for sales orders. As you can see below, the Sales Order List form has a collapsible section where you can specify criteria by all of the sales order fields, each having an operator selection for maximum flexibility.
At the bottom of the form lies the results grid with paged results displaying all sales order fields as columns. It also has a neat search criteria summary right above it, that shows the currently applied criteria. If you want to save the current criteria, so that you could retrieve this list faster, you can click on the PermaLink link next to the Reset button, and bookmark the resulting page.
You will notice, that the Sales Order Id column is hidden, but the first column has a link to the details view. Let’s click on that link to view or edit the sales order details. Again, the details view displays all fields of the sales order in two columns, as well as two tabs for its child lists - sales order details and sales reasons.
Note that required fields are automatically highlighted with bold labels here. If you clear some required fields and try to save, you’ll see validation errors displayed in red at the top, and the invalid fields will be also highlighted.
As you can see, with virtually no effort we were able to generate some basic, but pretty powerful, search and details forms out of the box. Granted that such basic forms will hardly be very useful as is, without any customization, unless they are really simple. We clearly don’t want to display all possible object fields, including internal ones like the row GUIDs. Nor do we want to view or edit internal raw IDs using plain text boxes. Therefore, in the following sections we will show you how to mold our model to display the forms the way we want them to look, as well as how to customize any generated code to make the application behave as we need it to.
2. Modeling the search view
As you saw in the previous section, the generated basic search form is showing all possible object fields in the results grid, and allows filtering by all of these fields. This is all based entirely on the structure of the "read list" operation that was generated in the model. The result fields are based on the output of this operation, and the criteria are based on the input.
Let's go ahead and update our model, so that the search screen would display only appropriate basic information about sales orders, and provide only the essential search criteria without overwhelming the screen, while the rest of the information would be displayed separately on the details view.
2.1 Modeling the results list
We will start with tailoring result columns in the search view by updating the output parameters of the "read list" operation on the "sales order" object. This is where we need to take a look at the output parameters, and decide which ones we want to remove, which ones we want to add, what is the order we want them to
go
in, and which parameters may need to use a different type from the type of the corresponding field on the object, so that they could be displayed differently on the screen.
The picture below shows parameters that we decided to remove in red boxes on the left, and the final structure of the output on the right, with the parameters that we want to keep, and their order within the output structure.
The dates on the sales order, such as the order date or the ship date, are stored as date/time in the database, and hence their fields are defined like that on the object as well. But it makes more sense to display them as just dates, without the time component, so we went ahead and overrode their type on the output parameters to be just “date”.
Also, in the Adventure Works schema, the customer is a business entity that may be either an individual person or a store, which could also have a contact person. To display that properly on the sales order list, we will show it as two separate fields: customer store and customer name. Since neither of them matches a specific field on the object, we will need to qualify them with a logical type (e.g. string).
Also note the "config" section of the output structure, which specifies that all these result parameters should be added as properties to the client data object SalesOrderList, which is defined separately in the model. This data object serves as part of the view model for our search view, and we will be updating it to further adjust certain UI behavior.
2.2 Adjusting fields’ visibility and labels, configuring links’ display
If you noticed before, our sales order list was not showing internal sales order IDs, which would not be very useful for the users. However, we still need to have those IDs in the result, in order to generate a proper link to the details screen, so it cannot be just removed from the output parameters. For such internal fields that we still need on the client, but don't want to display, the model provides a way to configure the corresponding client data object to hide them on the UI.
When we generated the CRUD operations for the sales order, it automatically configured the data object to hide a serial key, which is specified on the generator’s parameters as follows:
Let's navigate to the SalesOrderList data object in the model to see how you can configure a field to be hidden on the UI. If you are still in the output of the “read list” operation, you can just go to the data object class in the
xfk:add-to-object
element and hit F12, or select Go To Definition from the context menu, which will move you right to the declaration of that data object.
If you expand the definition of the SalesOrderList data object, you'll see that it has a
ui:display
element inside of it, where the “sales order id” parameter is configured to be hidden. You can also configure fields to be read-only here to make them generated as a text control, rather than as an editing control. For fields that are not hidden, you can set a custom label to use, if you don't like the automatic label derived from the parameter’s name. We will set the label for the "online order flag" field to be just Online as follows.
In addition to the display element, you can find two links under the SalesOrderList data object: one to open details of an existing sales order, and another one to create a new sales order. Inside the “details” link you can see that the model displays a validation error, telling you that the link cannot be displayed on the "revision number" field anymore, because we removed it from the output parameters. Go to that attribute in error, press Ctrl+Space to pull up the list of other non-hidden fields on the data object, and select the "sales order number" to display the details link on, as shown below.
2.3 Implementing custom result fields
If you remember, we added two custom fields to the results list that are not declared on the sales order object itself: customer store and customer name. The system would not know where they are sourced from, so it cannot generate code that would retrieve these values. What it does generate though is a set of placeholders in the generated service implementation code, where you can supply your custom code to retrieve these fields. They are marked with TODO comments to let you easily find all such places that need custom implementation.
Before we can provide the custom code, we first need to build our model, which runs all the generators. To do that let's right click on the AdventureWorks.Model project, and select the Build menu option. Next, we will expand the AdventureWorks.Services.Entities project and find the SalesOrderService class that implements our service contract. The ReadList method will have a LINQ query over our entities, where you will see the commented placeholders to implement between the CUSTOM_CODE_START and CUSTOM_CODE_END marker comments.
We will provide the custom implementation for these fields, which reads it from the related CustomerIdObject as shown below.
Note: if you cringe at the sight of such a generated name for the related child object as CustomerIdObject, then you can easily change that to be CustomerObject by renaming the field "customer id" to just "customer" on the "sales order" object.
You may be wondering if the custom code you have just written, which is mixed in with the generated code, will be erased next time you build the model and regenerate this class. As long as you
don't rename the result fields or operation in the model,
or change anything in the comments with the corresponding CUSTOM_CODE_START and CUSTOM_CODE_END markers, your custom code should be safe during the next model build. Essentially, the marker’s text is used to find the custom code when the class is regenerated, so you need to make sure it doesn’t change.
If you do need to rename the custom parameters or their operation in the model, make sure you make a copy of your custom implementation, or, better yet,
version control your code
, so that you could see any differences in the generated code after you regenerate the model.
Another danger to your custom code could be if you select Clean or Rebuild on the model project, or Clean or Regenerate on the corresponding Service Implementations generator. Any of these options will delete the generated files first, which is okay only if the files have no custom code, and basically make sense only if you have renamed some objects in the model, or plan to change the output path patterns for the generators, and would like to automatically clean up the old generated files and update the projects with new files.
In order to preserve the custom code during clean, please follow the instructions at the top of the generated file, which instruct you to delete a certain line in the header comment. For the service implementations you can also configure that in the model in the config section for the object by setting the
preserve-on-clean
flag to
true
on the
wcf:customize
element as follows.
Let's run the application and review the effect of our changes so far. Below is a sample screenshot of what it looks like now.
Notice how our result list has only the columns we specified in the output of the "read list" operation, with the details link on the sales order number, dates showing without a time component, and the customer store and customer name populated as appropriate.
2.4 Modeling the search criteria
Now that we have modeled our list results, let's move on to modeling the search criteria. They are defined in a structure with a name "criteria" inside the input of the "read list" operation. With all the operators and second values for the BETWEEN operator, the current criteria for all sales order fields won't fit on one page, even when collapsed to definitions, so we're not going to show them here. We will first need to delete any criteria that we don't need, so it’s good that we have them all, since reviewing all the criteria and then deleting unnecessary ones is so much easier than just adding them from scratch.
Deciding which criteria to keep may be even more important than picking the result fields, since they directly support the business use cases. Here are some sample use cases that we will use to determine which criteria we need:
-
Looking up a specific sales order by a sales order number.
-
Searching sales orders in a certain status.
-
Finding orders placed during certain time period.
-
Finding orders that are past due or due soon.
-
Finding large orders and small orders by the total amount.
-
Finding orders by the customer store name or individual name.
-
Searching orders within certain geographical regions and territories.
-
Searching orders placed by specific sales people.
This is what our model will look like for these use cases.
As with the result fields, we will override the type on the date criteria to be just date without the time component, as well as set the type on the custom criteria "customer store" and "customer name" that don't have a corresponding object field. We have also added operator parameters for our custom criteria, and removed the second values for numeric criteria that don't need a BETWEEN operator, such as “status”, “territory id” and “sales person id”.
Notice also the green config section, which specifies that all these parameters should be added to the client data object SalesOrderCriteria that the search criteria panel will be bound to. This data object is where we will be able to configure criteria labels in the model.
Remember that when we added custom result fields to the output, we had to also add custom code to the service implementation to retrieve these fields. Since the custom criteria we just added have exactly the same names as the corresponding result fields (“customer store” and “customer name”), Xomega was able to generate code that simply filters on those results fields, so we don't need to provide any additional custom code.
With that, we can just go ahead and build the Model project, and then run the application. We should get a screen that looks like this now.
You should be able to recognize our modeled criteria on the screen, see that the dates have no time component, and verify that filtering by our new customer fields works as expected.
2.5 Defining static enumerations
When you look at the status column on the Sales Order List screen, you will notice that it shows internally stored numeric values instead of user-friendly status descriptions. The list of possible status values and their descriptions is static, and is not stored in any of the database tables. Right now you can only find it in the documentation stored on the Status column of the SalesOrderHeader database table, which was imported into the model as shown below.
Xomega model allows you to describe this information in a structured way, and use it in the generated screens. It also generates constants in the code that you can use instead of hardcoding these values. So let's see how we can enhance our model with static lists of values like the one for the sales order status.
We will start by declaring an enumeration named "sales order status", and listing all its items with their respective names and values. You can also define and set additional properties for enumeration items, and even inherit one enumeration from another, but, for our simple status enumeration, this basic setup is enough.
Next, we will declare a new logical type, which we can also call "sales order status", and will add a reference to our enumeration inside of it. Since the original type of the status field is "tiny int", we will set the base type for our new type to be "
tiny int enumeration
", which is declared in the framework, and combines various enumeration configurations with the “tiny int” configurations.
As you add the new type, you will notice a warning on it, telling you that this type is currently not used in the model. So the logical thing to do next will be to set this type on the “status” field of the “sales order” object. The following picture illustrates the resulting model, and highlights each of these steps.
To provide another example, let's define one more static enumeration, which will be based on string values now, rather than on numeric values. This is also typical if you store short codes that have longer decodes, e.g. a list of states with NJ as a code and New Jersey as a decode.
If you open sales_territory.xom file, then you will see that sales territories have a group field, which specifies one of the territories’ global regions: North America, Europe and Pacific. This field is a unicode string of up to 50 characters long, which we need to keep in mind when defining our enumeration.
As before, we will define an enumeration "sales territory group" with its values listed, then declare a type with the same name, referencing this enumeration, and finally use that type on the "group" field of the sales territory object. We will inherit our new type from the string-based "enumeration" type, but to preserve the database properties of our original type, we will set its size to 50, and SQL type to nvarchar, as shown below.
Now, the reason we've added this territory group enumeration is to be able to add another criteria parameter "global region" to the read list operation on the sales order object, which would use our new type as follows.
Unlike other criteria, we did not add a separate operator parameter for simplicity, which implies the equality operator will be used whenever this value is selected.
Because this criteria parameter does not match any of the object’s fields, nor does it match any of the output parameters, we will need to provide a custom implementation on how to use it in the implementation of the “read list” operation of our service.
Let's build the model, and then navigate to the ReadList method of the SalesOrderService class, and
provide the custom code
for filtering by the global region as follows.
Finally, let's build and run the application to review our changes for the static enumerations. The following screenshot shows what our sales order list screen will look like now.
Notice that the status column shows decoded values, and the status criteria displays a dropdown list to select the status. Also, we have a Global Region dropdown list, which allows filtering sales orders by the
specified sales territory group
, without having to select an operator first. We will see how to turn the Territory Id and Sales Person Id into enumerations in the next section.
2.6 Defining dynamic enumerations
In the previous section, we have seen how to define static enumerations in the model, which have a fixed list of values that doesn't change within any specific version of the application. In addition to static enumerations, applications also typically have dynamic enumerations based on some reference data. The lists of values for dynamic enumerations are often small enough to allow selecting values from a dropdown list or a list box. The lists may change over time, but this generally happens rather infrequently, which warrants caching them in the application.
In our project, a list of sales territories is a good example of such a dynamic enumeration. Instead of showing or entering internal numeric territory ID on the sales order screens, we would like to show or select the territory name that is defined on the sales territory object.
In a nutshell, defining dynamic enumerations in the model is similar to defining static ones, except that instead of explicitly providing a list of values in the model, you need to configure a “readlist” type of operation with an
xfk:enum-cache
element, where you can give enumeration a name, and indicate which output parameter returns the ID, and which parameter returns the description for each enumeration element.
Xomega makes it really easy by providing a special Enumeration Read List generator for such a model enhancement. Let's go ahead and run this generator on the sales territory object, as shown below.
Once you have run the generator, you will see that it added a single “read list” operation to the object without any other CRUD operations, and decorated it with the enumeration specification. Since the result of the operation will be cached, we’ll want to remove any extraneous parameters to minimize the amount of data in the cache. We will only leave the ID and description parameters, and a couple of other important attributes such as the territory group, which will allow us to do cascading selection later on. The following picture illustrates what the model will look like after that.
You can also see that it also changed the "sales territory" key type to inherit from the "integer enumeration" type, and added to it a reference to the new enumeration.
Now that we understand the structure of the model for dynamic enumerations, let's set it up for another object "sales person", which will have a couple if additional twists to it.
The problem is that the key type "sales person" inherits from another key type "employee" for the employee object, which tells the model that a sales person is a type of employee, and establishes implicit zero-to-one relationship between the two objects. Therefore, we don't want to change the base type for the “sales person” type, and inherit it from "integer enumeration", since we don't want to break this relationship. We can configure the Enumeration Read List generator to leave the key types alone by setting
Make Key Type Enumerated
property to
False
, as shown below. (Note, that you’ll need to set it back after running the generator if you're not planning to keep it this way.)
Next, let's run the Enumeration Read List generator on the sales_person.xom file.
As before, this will add a “read list” operation to the “sales person” object, decorated with the enumeration specification. We will strip it off of any extraneous parameters, except for the key and the territory id, which we can use for cascading selection.
Since the “sales person” object does not have any suitable fields for the display-name of its own, we will add such an output parameter to the operation, and will set it as the description parameter for our enumeration. We will also add “is current” output parameter, which indicates whether or not this is a currently employed salesperson, and will use it for the
is-active-param
attribute on the enumeration. This will allow displaying only active/current salespersons in any selection lists, while still using any inactive items to decode salesperson ID to their name.
Because the key type was not updated to inherit from the “integer enumeration” type, for the reasons that we discussed earlier, we will need to add any relevant configurations from the “integer enumeration” and “enumeration” types to the “sales person” type, specifically the Xomega Framework property type, and the edit web controls for single-value and multi-value properties. And, of course, we will need to add a reference to our dynamic enumeration in there as well. The following picture illustrates this model.
To add a custom implementation for our “name” parameter, we will build the model first, and then update the ReadList service method on the generated SalesPersonService class as follows.
Let's also allow filtering sales orders by multiple salespersons, by making the “sales person id” criteria multi-value. You do it by simply
setting the
list
attribute
to
true
on the corresponding input parameter element of the sales order’s “read list” operation, as follows.
Finally, we will update the labels for the “sales person id” and “sales territory id” criteria and result list columns, since they will no longer display the internal ID.
Now, following already familiar procedure, we will build the model again to regenerate all the artifacts, and then run the application. The Sales Order List screen will now look as depicted below.
As you see, both Sales Person and Sales Territory show their display names in both the results list and the search criteria. The Search Criteria panel also features a drop down list to select a sales territory,
and a list box to select multiple salespersons
, and the filtering automatically works for the multi-value criteria.
2.7 Using auto-complete with enumerations
Sometimes, for relatively large enumerations, it is better to give the users a text field to enter the values, where they can type in partial names, and then select the value from a smaller, filtered drop down list. The list may show full names, but when the user selects an item, an internal value will be inserted into the text field. For example, the drop down list may display a list of state names, such as New Jersey, but the text field will have the 2-character code NJ, which is used internally by the system.
To demonstrate how to configure auto-complete in the model, we will use the order status criteria, which we will change to accept multiple values in a text field.
This may not be the best example, since the list of statuses is pretty small, and their internal codes are numeric, and not that user friendly, but it should be okay for the demonstration purposes.
Let's go ahead and add the
list
attribute to the “status” parameter of the sales order criteria, as shown below.
Next, we will update the configuration of the "sales order status" type in the model to override the web control used for multi-value properties to be the TextBox, as opposed to the ListBox inherited from its base type "tiny int enumeration".
Finally, we will want to update the display format for the status values in the selection list to display both the code and decode, e.g. Code - Decode. This will allow the selection list to be filtered correctly when you drop it down for an existing value, which would be just the code.
This must be done in the custom code for our criteria data object, which gives us a great opportunity to show you how to customize generated data objects. We will start by finding the definition of the SalesOrderCriteria data object in the model, and setting its
customize
attribute to
true
as follows.
After that, let's build the model project, and navigate to the generated SalesOrderCriteria data object class under the AdventureWorks.Client.Common project. If you expand it in the solution explorer, you will notice that it has a SalesOrderCriteriaCustomized class nested under it now. Open it up, and set the display format of the generated StatusProperty in the OnInitialized method as follows.
That's all there is to it. If you run the application now, you will see that the status operators have been updated for the multi-value properties, and the selection control has been changed to a textbox with a drop down list, which displays the statuses using the display format that we specified.
2.8 Configuring cascading selection
In this section we will show you how to configure cascading selection for enumeration properties, where a list of possible values for one property depends on the selected value in another property. This also needs to be done in the custom code for a data object, but Xomega Framework’s support makes it extremely easy.
We will make the list of sales territories on the SalesOrderCriteria cascade off of the selected global region (territory group), and the list of sales persons cascade off of the selected sales territory. Let's open the SalesOrderCriteriaCustomized class that we generated in the previous section, and add the following code to its OnInitialized() method.
As you see, Xomega Framework allows us to do it in just one line per property. Instead of hard coding names of enumeration attributes that are used for cascading, such as “group” or “territory id”, we were able to specify them using constants, conveniently generated for us by a Xomega generator based on those additional parameters that are returned by our dynamic enumerations.
The application needs to update the cascading list of values as soon as the value it depends on is changed. Due to the ASP.NET Web Forms architecture, this requires the AutoPostBack attribute to be set on the controls for the global region and the territory criteria, so that the changes in their values could trigger updates of their dependent lists. This can be done in the custom code for the generated views, but Xomega model provides an easier way for that. All you have to do is to set the
is-trigger
attribute on the corresponding fields in the data object definition as follows.
Given that this change in the model only affects the views, we can regenerate just the ASP.NET Views to make it faster, instead of building the entire model project, which would run all generators.
Run the application now, and try changing the global region and the sales territory.
As you change the global region, you will see that the list of sales territories will be updated for that region only. Similarly, once you select a sales territory, the Sales Person selection will be narrowed down to that territory only, and also to sales persons without any territories. From the screenshot, you can see that there are only four such sales persons for the Northeast territory.
2.9 Adding custom UI validations
The generated UI data objects for our application already provide validation of individual properties, such as when the field is required or when the supplied value was not in the correct format. However, you may need additional UI validations, including cross-field validations.
Let's add a validation to our customized SalesOrderCriteria class, that will check that the From date is not later than the To date for the Order Date criteria, and will add a validation error otherwise. We will override the Validate function on the customized data object for that as follows.
Now if you run the application, and specify invalid From and To order dates in the criteria and hit Search, you will see our validation error displayed.
Note that so far we have been adding all that custom code to the generated data objects, which are reusable between other clients, such as WPF clients. This means that all these custom validations, cascading selection, etc. will be also available in the corresponding WPF desktop applications out of the box. This is just an example of how Xomega Framework helps you write reusable code.
3. Modeling the details view
So far we have generated basic search and details views from a Xomega model that we imported from a database, and then configured our model further to generate a more useful and relevant search screen. Along the way, we have learned a lot of important Xomega concepts and methods, such as how to customize the generated code, define static and dynamic enumerations, and so forth, which, going forward, we will apply for other things in our tutorial.
Now, let's transform the sales order details form into a more usable screen using both techniques that we have already learned, and some new ones that are more pertinent to the details screens.
3.1 Updating details child list
With all the changes that we have already made to the model, let's run the application and review what the Sales Order details screen looks like now.
You will notice, that the fields, for which we have defined static and dynamic enumerations in the model, such as Status, Sales Person and the Sales Territory highlighted in green, already have drop-down list controls for selecting a value, which display a user-friendly name.
We will start improving the Sales Order details screen by updating the child table with the order line items, which shows the products ordered, their quantities and prices. Looking at the current table, the following updates come to mind.
-
Display Product using product name as the first column, which will have a link to the line item details, and move the Carrier Tracking Number to the end.
-
Display special offer using the offer description instead of the internal ID.
-
Remove Row GUID and Modified Date internal columns
-
Display the Line Total in the currency format.
We will turn the list of products into a dynamic enumeration, since it does not change very often, and is not too large to be cached. Make sure that you set the
Make Key Type Enumerated
property of the “Enumeration Read List” generator to True, so that it would automatically configure the key type. Next, open up the product.xom file in the model, and run that generator on that file, as shown below.
Expand the generated “read list” operation on the “product” object, and remove any extraneous parameters, except for the product ID and name, which are used as the enumeration's ID and description parameters respectively, as well as the product subcategory ID and the product model ID, which we could use for cascading selection. Also, add a required boolean parameter "is active", which will indicate if this is a current product that has not been discontinued. We will also set this parameter as such on the enumeration configuration, so that it would not prompt discontinued products in any selection lists, but can still use them to decode a product ID into the corresponding name. Your “read list” operation will look as follows.
To add implementation for our “is active” parameter, we will regenerate all code by building the model, and then insert the following custom code into the generated ReadList method of the ProductService.
We will do similar steps for the “special offer” object, generating a “read list” operation decorated with an enumeration declaration.
We will strip it off of any extraneous parameters except for the ID, description, and the category attribute for cascading selection. We will also add "is active" parameter to indicate current special offers, and will set it on the enumeration as well.
Lastly, we will build the model again, and will provide the custom code for the “is active” parameter implementation, as follows.
Now that we have done all these preparations, let's open the “read list” operation on the "detail" sub-object of the "sales order" object. We will remove the Row GUID, and the Modified Date output parameters,
drop “id” from the names of “product” and “special offer” parameters
to make the proper text on their column headers, and reorder other parameters properly, as displayed below.
Note that these parameters will be added to the SalesOrderDetailList data object, which we will use to update the labels for the details table columns later. For now, let's address the issue with the formatting of the “line total” field. You can see that this field has been imported with a generated type numeric_38_6, which even has a warning saying that it extends a deprecated type "numeric". You can see that warning if you go to that type's definition, or whenever you build the model.
If you right click on the type definition, and select Find All References, you will see that this field is the only place that uses it. Therefore, we will just rename this type to be "line total", move it to the same file sales_order.xom, and will make it inherit from the "money" type to make sure that it gets formatted as currency. We will keep the SQL type configuration override, so that it keeps the mapping to the original SQL type, as shown below.
Next, let's open the SalesOrderDetailList data object, and provide some custom labels for our columns. We will also update the “details” link to be displayed on the field “product”, as follows.
Let's build the model one more time, and provide the custom implementation for the product and special offer output parameters, as follows.
If you run the application now, you'll see that the Sales Order Detail grid looks exactly according to the requirements that we defined earlier. Below is a picture that illustrates this.
As you see, it only shows the columns we specified using our labels as column headers, the product and special offer show names instead of IDs, and the Line Total field is formatted using currency format.
3.2 Handling read-only fields
If we look at the details screen generated for us by default, we will notice that all object fields there are editable. However, some fields are completely internal, and should not even be displayed to the user, such as the Row GUID highlighted in red below.
Other fields, such as the ones highlighted in yellow, are set internally by the system, and should be read-only on the screen. To address these issues, we will need to properly update the generated CRUD operations to specify which fields can be updated on the object.
We will start by updating the output parameters of the "read" operation for the "sales order" object. Since it is the first structure in the model that is added to the data object SalesOrderObject, and includes all properties of that data object, the order of its output parameters will drive the order of the fields on the screen, so we can use it to rearrange some fields on the screen as well. Let's remove the "rowguid" parameter, move the “
sales order number
” to be the first parameter, and move the “revision number” to go right before the “modified date”. We'll also override the type on the "order date" parameter to be just "date", without the time component. The following picture illustrates this setup.
Next, let’s remove the “rowguid” parameter from the input of the “create” operation, and move the other parameters, that are calculated during order creation, to the output of the “create” operation, as shown below.
Note, that we also need to update the type of the “order date” parameter here to be “date”, so that it would be consistent with the type of this parameter in the “read” operation. In a similar manner, we will remove all those parameters (including “rowguid”) from the input of the “update” operation, and move the following two parameters to the output of the operation, since they are changed on each update.
As you update operations’ inputs and outputs, make sure to insert the config section with the
xfk:add-to-object
element in them as needed. This will allow the data object to set the input parameters from its properties when calling the operation, and to update its properties from the output parameters when handling the operation result.
The system may set the read-only parameters in different ways. For example, the Sales Order Number is just a computed field in the database, and the Revision Number is incremented by a database trigger. The Row GUID, Modified Date and Order Date, which would presumably be the order creation date, are not updated in the database, so we will add custom code in the Create method of the SalesOrderService to populate them as follows.
And similarly, we will add custom code in the Update method to set the modified date.
Let's build the model, and run the application to review the results. Here's what the Sales Order Details panel will look like.
Notice how our fields are displayed as read-only labels, and in the correct order now, while the Order Date is shown as just a date.
3.3 Grouping fields as child data objects
When working with such a large data object as the SalesOrder, it makes sense to break it up into groups of related fields, and display them in tabbed child panels. For example, the fields highlighted below in yellow are related to the payment information, and can be grouped under the Payment tab.
Let's update our model, so
that these fields would be displayed in the Payment child panel.
Before we do that though, we can quickly turn the Shipment Method into a dynamic enumeration to allow selecting it from a list of possible shipping methods. As usual, we will run the “Enumeration Read List” generator on the ship_method.xom file, and update the output of the generated “read list” operation to return only the ID and name, as shown below.
Turning this group of fields into a child object will require more advanced level of modeling, but it's still pretty straightforward once you get the hang of it. We will start by declaring a new
xfk:data-object
in the model with a class SalesOrderPaymentObject. Once you do that, you will get a model warning telling you that it doesn't have any properties defined, which we will fix next.
Right above the top level "objects" element, we will add a
structs
element, and define two structures named "payment info" and "payment update" that are based on the "sales order" object, and will configure them to have their parameters added to the SalesOrderPaymentObject data object.
The first structure, "payment info", will represent the payment fields that we read from the sales order service, so we will go ahead and move all those read output parameters to this structure. Again, since it has all parameters for our data object, and goes first in the model, the order of these parameters will determine the order of the fields on the screen. While we're at it, we will override the type for the "due date" parameter to be just date, and set the type of the "currency rate" to be a string, so that we could return a user-friendly description of the currency rate.
The second structure, "payment update", will represent a smaller subset of the fields, which can actually be set by the user. This will not include any amounts and the currency rate, which are calculated internally by the system from other data. We will use this structure in both create and update operations. The following picture illustrates this setup.
Next, let's add our SalesOrderPaymentObject as a child of the SalesOrderObject with a name "payment". We will also configure the label for the "ship method id" field to be Shipment Method, since we turned it into a selection control, as shown in the picture below.
Finally, we will move the “ship date” parameter down, and replace
the corresponding output parameters
in the "read" operation of the sales order object with a reference to the "payment info" structure using the same child name.
And similarly, we will replace
those same
input parameters in the “create” and “update” operations as the ones we removed from the “read”, with a reference to the "payment update" structure with the same name. We’ll also move the “ship date” parameter down as shown below.
Now
that we have updated our service model to return and accept child structures like this, we will need to provide custom implementations for handling these parameters in the generated service. However, those will be pretty large pieces of custom code, which we don't want mixed in with the generated code. In addition, the custom code for the “update” and “create” operations will be pretty much the same, so a better design will be for us to extend the generated partial class SalesOrderService with our own file, where we can implement this custom code in separate methods, and mix in the calls to those methods in the generated file.
The easiest way to do it is to set the
extend
attribute to true on the
wcf:customize
element of the sales order object configuration, as shown below.
Once we build the model, we will notice that there is a SalesOrderServiceExtended.cs file nested under the SalesOrderService class, which provides extension of the partial class for our custom methods. Let's open it up, and add a GetPaymentInfo method that populates and returns a PaymentInfo structure for the given SalesOrder object as follows.
Next, we will add a reusable method UpdatePayment, which takes input data as a PaymentUpdate structure, and updates the SalesOrder object provided within the specified context. This method can accept both new and existing SalesOrder objects, and therefore can be used from both create and update operations. The snippet below shows what it would look like.
All that's left to do now is to set the corresponding custom code for the Read operation to call the GetPaymentInfo method as follows.
For Create and Update operations we will set the custom code to call the UpdatePayment method, as shown below.
Let's run the application, and review the updated Sales Order details screen. The following picture illustrates this.
As you can see, our fields have been moved down from the main panel to a separate Payment tab, where they are stacked up in the order they were listed in the "payment info" structure. The fields that are not listed in the "payment update" structure are displayed as read-only labels. Shipment Method with updated label is a dropdown list with available shipments methods, the Due Date is displayed as a date only, and the Currency Rate is shown as a string that we build in our custom method.
3.4 Using a multi-value property for a simple sub-object
Multi-value properties on a details screen are usually modeled within relational database design as simple child tables, which have a reference to the parent object and a single column for the values. The list of sales reasons on a “sales order” object is a very good example of such a design. Let's have a look at the following screenshot.
The list of sales reasons has the main column with the reason ID, and an audit column with the modified date. What we want to have on this screen for editing sales reasons is a convenient multi-select control showing reason names instead of the current table. We also want it on a separate child panel on this screen grouped with other sales-related fields, such as the Sales Person and Sales Territory, also highlighted in yellow.
We will start by removing any elements related to the “reason” sub-object, that were added for us by the Model CRUD generator, such as the operations, data objects and views, as shown below.
The easiest way to also clean any previously generated artifacts for those removed elements is to rebuild the model, which runs a model Clean command followed by a Build command. This will delete those generated files, and will remove them from their projects.
Before you do anything like that though, you want to absolutely make sure that any generated classes containing custom code will be preserved during the Clean. If you previously set
preserve-on-clean
attribute to
true
on the
wcf:customize
element for the “sales order” object, as we showed before, then you don't need to do anything now. Otherwise, you can remove some lines in the top comments in your customized files, as described in those comments. It is also a good idea to
check everything in
your
version control
before you run the Clean or Rebuild commands on the model.
After we rebuild the model, we will want to define a dynamic enumeration for the “sales reason” object by running the “Enumeration Read List” generator on the sales_reason.xom file. In addition to trimming the output of the generated “read list” operation to keep just the “reason id” and “name”, we will override the web control that is used for multiple selection on the “sales reason” type. We will use a user control uc:PickListControl (notice the xmlns:uc namespace at the top) provided with the Xomega solution template, which has two lists and buttons to move items from one list in another. The following picture illustrates this setup.
Next, we'll follow the technique for grouping fields in a child panel that we described earlier. We will declare a new data object in the model with a class SalesOrderSalesObject, and then a new structure "sales info", whose parameters will be added to that data object, as shown below.
Notice, that we marked the sales reason parameter with the
list
attribute set to
true
to make it a multi-value property. Since we have dynamic enumerations for all of these fields, which can decode IDs to names, we don't need to read anything else in addition to these IDs, so we can use the same structure for both “read” and “update” operations.
On the SalesOrderSalesObject data object we can specify proper labels for the territory and the sales person, set the
customize
attribute to
true
, in order to set up cascading selection for them, and mark the territory as a trigger to enable AutoPostBack.
To finish up the model updates, we will add the SalesOrderSalesObject as a child of the SalesOrderObject under the name sales.
And then we will replace territory and sales person parameters in the output of the “read” operation, and input of both “create” and “update” operations, with a reference to the "sales info" structure using the same name.
After that we will build the model, and then add the following method to
the extended SalesOrderService class
, which populates the SalesInfo structure for the given sales order.
We will also add another method that updates the specified sales order with the provided sales info structure as follows.
Notice how the method adds or removes sales reasons to the child list based on the supplied new list of reasons.
After that we can use these methods in the placeholders for the corresponding parameters in the generated service implementation class. The Read method will be updated as follows.
And both Create and Update methods will be updated like this.
To set up the cascading selection of the territory and the salesperson, we will add the following code to the customized subclass of the SalesOrderSalesObject class.
Let's run the application again, and review our changes.
As you see, instead of the Reason tab, our sales order details screen has now a Sales tab, where our sales info fields are displayed. The Sales Reason field has a convenient selection control to pick one or more reasons for the sales order.
3.5 Showing fields from related objects
In this step we will demonstrate how to show fields from related objects on a details screen using techniques that we've learned so far. The customer related fields, highlighted in yellow on the Sales Order panel below, are just the internal IDs of the related customer and address objects that are stored on the sales order object, and were added to our screen by default. What we need to do is to move them into a child panel Customer, and display the relevant customer fields instead of the IDs. In the next step, we will also show how to enable selection of a customer ID using a lookup form.
Similar to the steps that we did before, we will add a new data object SalesOrderCustomerObject, and
will define two structures in the sales_order.xom file
that will be added to this data object - "customer info" for reading customer information, and "customer update" for updating customer information on the sales order. The following picture shows these structures.
They both have object “customer” as the base object, so any parameters that don't have a matching field on the “customer” object will need to be qualified with a type. If you remember from the list screen, a customer object consists of a store and/or a person, so we will add the store name and the person’s name to the "customer info" structure to be read.
Next, we will configure those internal IDs to be hidden on the screen, and will set the label for the “
territory
id” parameter as follows.
We will then add this data object as a child to the SalesOrderObject under the name "customer" like this.
Next, we will replace
those
internal ID parameters (i.e. “customer id”, “bill to address id” and “ship to address id”) on the output of the “read” operation, and the input for the “create” and “update” operations with a reference to the "customer info" and "customer update" respectively.
We will also set the type for the “ship date” parameter to be just “date” without the time component in both cases.
Here is the related part of the “read” operation’s output.
And the input for the “create” and “update” operations will look like this.
Let's build the model, and add the following method to the
extended SalesOrderService class
to populate the customer info structure from the SalesOrder object.
As you see, the structure parameters are just populated from the various related objects' fields. We will also add the following method to update customer and address objects on the SalesOrder object from the provided CustomerUpdate structure.
Finally, we will insert invocation of these methods into the corresponding placeholders on the generated service implementation class for the Read operation as shown below,
as well as for Create and Update operations as follows.
Let's run the application to review our changes. The following screenshot shows what the Sales Order details screen will look like now.
Notice that we have a child tab Customer with the customer information, and that the Ship Date has no time component anymore. Since the internal customer ID is no longer visible on the screen, and hence not editable either, we will need to provide another way to select a customer for a sales order, which we will do in the next step.
3.6 Building a lookup form for selection
When creating sales orders, one of the main things the user needs to do is to select a customer for the order. Unlike other enumerated fields, such as a sales person, which you can select from a relatively small list that can be cached on the client, there could be potentially thousands of customers to select from, which doesn't lend itself to selecting them from a drop-down list or caching them on the client.
What we really need is a way to look up the customer by some attributes like names, or a specific account number, and then select it for the current sales order. In this step we will show you how Xomega makes it extremely easy when leveraging all the techniques that you've learned so far.
First thing we need to do is to create a customer search view similar to how we've created a search view for sales orders. However, in this particular case, we're interested in only the search view for customers, without any details views. So, to enhance the customer object in the model with just Read List operation and views without CRUD operations, we will clone the existing “Full CRUD with Views” generator by selecting it and pressing Ctrl+C.
Now we can give it a name Lookup View, open its properties, and turn off the
Generate CRUD
flag in the Operations group as follows.
This is a way to save a custom configuration for a generator as a separate generator in the model project. After that, let's go ahead and run the Lookup View generator on the customer.xom file as shown below.
The generator will add the necessary model elements to configure the customer search view, but we will want to update the input and output parameters for the generated read list operation, so that the view would accept the criteria that we want, and show the result columns that we need. We're going to add all parameters from our "customer info" structure to the output of the read list operation, and have criteria with operators by these parameters. The following picture illustrates this setup.
Since we have custom output parameters, we will need to build the model to regenerate the services, and provide custom implementations
in the CustomerService.cs file
as follows.
Now that we have the Customer List view ready, we need to add a link to that view from the Sales Order details screen. Since the link will be to select a customer, we will add it to the SalesOrderCustomerObject data object under a name "look up". To make the link open the customer list view for customer selection, we will set the
child
attribute to
true
on it, and will add parameter
_action
with a value "select", and parameter
_selection
with a value "single".
To map the columns of the selected record to the data object properties to be populated from it, we will also add result parameters to the link for each customer list output parameter used, and indicate the fields on the current object that these output parameters will populate. To make this mapping process easier Xomega provides Intellisense for the values in parameter names and the source or target fields.
The following picture shows what this setup will look like.
If we build the model now, and run the application, then we will see that the Customer tab will have a Look Up link now, which opens up the customer list screen as a child dialog, where you can search for customers using our criteria, and select a specific customer, as shown in the picture below.
Technically, this fulfills our requirements for allowing to select a customer, but this process could be a little cumbersome, since you always have to pull up the customer list screen, populate the criteria, and run the search, in order to select a customer. What would be better is if you could enter some common lookup criteria, such as the store and the person names, right on the sales order details screen. If they match exactly one customer, the Look-Up button would populate its values without even popping up a selection dialog. If they match no customer or multiple customers, then the link will open up the customer list dialog with the specified criteria and results pre-populated.
To implement this, let's define another data object with a class SalesCustomerLookupObject, and add it as a child to the SalesOrderCustomerObject. We'll move the link to that new object, and will update the result parameters to populate the fields of the parent object by adding
data-object=".."
attribute to them as shown below.
To add properties to this data object we will define an auxiliary structure with “store name” and “person name” parameters added to this data object, as shown below. Notice how we marked this structure with a
generic
usage attribute to suppress a warning from the model telling us that this structure is not referenced anywhere.
We will use these properties as source fields for the link parameters coupled with corresponding operator parameters, for which we will use a specific value CONT for the Contains operator, as shown above. This way, the search will automatically look for customers, whose store or person name contains the user specified string.
Finally, let's build the model, and run the application to review our changes. The picture below demonstrates what the screen will look like now.
As you see, the Customer tab has a child Lookup panel with the store name and a person name to look up the customer by.
If
you enter a store name that matches multiple customers and click the Look Up link, then you will get a customer list dialog pre-populated with the matching customers to select.
3.7 Adding contextual selection
If we take another look at the Customer tab, we will notice that we still have the Bill To and Ship To address fields displayed as editable internal IDs that point to stored address entities in the normalized AdventureWorks database, as shown below. This is clearly not how we want the users to select these values.
Normally, we would want the user to select the stored address from a list for the current customer, which is defined in the "business entity address" association object. However, there are tens of thousands records in this association table, and we don't want to load them all into a cache, like we did with dynamic enumerations previously. It is also not too user-friendly to make the user select the address from a separate look-up form, as we did for the customer selection.
What we want is a contextual dropdown list, which would use the currently selected customer to read the list of associated addresses. To implement populating of such a contextual list, we can leverage many of the existing Xomega features, but we will need to write some custom code to glue it all together, as you'll see below.
We will start by opening the business_entity_address.xom file, and manually
adding
a "read list" operation that returns address fields using their types from the corresponding objects,
and has
a dynamic enumeration defined, as follows.
The main difference from the dynamic enumerations that we've created so far is that we now have input criteria for the operation, which consists of the required business entity ID. This will ensure that the operation returns addresses only for the specified business entity. We’ll use the “address id” and the “address type” as the ID and description parameters respectively. Also notice the special
skip-registration
flag that we set to generate cache loader class that we need to subclass, without registering it for this enumeration.
Technically, we need to use the same logical types for input and output parameters here, as the types for the underlying fields on the corresponding objects. But to bring some sanity to the model, you will want to rename the default types that were generated by the import process, and give them more meaningful names. You can do it right here where you use these types by selecting Rename from the context menu, or by pressing Ctrl+R,R. This will bring up the Rename dialog, and will display all references of the selected type that will be renamed.
The following picture demonstrates this for the string60 type, which, as you can see, is used only by the address line fields.
If the generated type is used by different type of fields, you can refactor it into a new type for your field with the same setup, but a more appropriate name. Here is what our parameters will look like after the refactoring.
Normally, you'll want to move the new specific types to the same files where they are being used the most. In our case we will move the address related types to the address.xom file, which will look like this.
Notice that we changed the base type for the address to be “integer enumeration”, so that it would use enumeration related properties and controls.
Finally, since we need to add custom code for the contextual address selection, we will set the
customize
attribute to
true
for the SalesOrderCustomerObject to generate a customization subclass.
Now that we have set up our model, let's build the model project to regenerate the services, views and data objects. Once they are regenerated, we will need to provide the customs service implementation code for our new BusinessEntityAddressService. First and foremost you want to implement filtering by the required input criteria, namely the BusinessEntityId, as follows.
If you forget to do this, your service will return tens of thousands of records, and trying to display so many records in a drop down list will quickly bring the UI to its knees.
Next, we will provide custom code to populate the output fields from the related objects as follows.
In general, what we need to populate the list of addresses for the current customer on the sales order, is to read it from our service whenever the customer changes, construct a lookup table from the results, and set them on the corresponding address enumeration properties. For that we want to leverage the lookup cache loader that was generated from our “business entity address” enumeration that we set up in the model.
Let’s define a subclass of the generated cache loader called AddressLoader in the customized SalesOrderCustomerObject, and pass that customer object to it in the constructor. In our subclass we will override the ReadList method to pass the business entity
ID
of the customer's store or person, if this is an individual customer. We will also add a property change event handler for the person and store Id properties, which will invoke this operation asynchronously, and will update the bill-to and ship-to address properties with the resulting LookupTable. The following snippet illustrates this class.
With that done, all we have to do is to construct this AddressLoader on the current SalesOrderCustomerObjectCustomized class during initialization, which would subscribe for that object’s change events, and perform any proper updates. If you want to manage this subscription, you’d need to store a reference to this new AddressLoader object.
If we run our application now, we'll see that the address properties display dropdowns with different types of addresses for the current customer, as shown below.
This setup is much more usable now, since you can select one of the stored addresses for the current customer instead of entering their internal ID. What is missing though, is ability to view the details of the selected address beyond the address type, such as the street address, city, state etc. All this information is already returned from the service, and stored as attributes of the selected value, so we just need to show them on the screen as separate fields.
It makes sense to group the address fields for each type of address in its own child panel. To configure that in the model we will use the techniques that we have learned earlier. First off, we will declare a data object AddressObject in the address.xom file of the model, and then we’ll define two structures contributing their parameters to this data object - one for updates with just the address key, and another with the full address info, as shown below.
The purpose for the address info structure is to add more fields to the AddressObject, since we're not going to use it in any operations. Therefore, to suppress a warning that this structure is not being used, we have marked it as generic.
For the same reason, since the system cannot determine if its fields are editable based on whether or not this structure appears in the input of any of the operations, we will need to explicitly mark all its parameters in the AddressObject as not editable to generate data labels instead of edit controls for these fields. We will also customize this data object, so that we could add the logic that populates all these fields from the attributes of the selected address, and will mark the “address id” field as a trigger for all these updates, as shown below.
Next, we will replace the bill-to and ship-to address ID parameters with references to our new "address key" structure in the “customer info” and “customer update” structures.
And finally, we will add the AddressObject as a child of the SalesOrderCustomerObject for both the billing and shipping addresses, using the same names, as we used in the customer structures earlier.
Now that we have made all the model updates, let's build the model project, and refactor our custom implementations in the SalesOrderService to use the new AddressKey structure. Below is the change in the GetCustomerInfo method.
And here is the corresponding change in the UpdateCustomer method, using the new BillingAddress and ShippingAddress structures.
We also need to update our custom lookup cache loader in the SalesOrderCustomerObjectCustomized.cs to take the AddressIdProperty from the corresponding child object as follows.
The real change though will take place in the customized data object AddressObjectCustomized, where we will subscribe to the Change event for the AddressIdProperty, and populate other address properties from the corresponding attributes of the selected address Id value, as follows.
Let's run the application now, and check out the customer panel on the Sales Order details view. The picture below shows what it will look like.
As you see, we have turned it into a full fledged customer panel, where you can look up the customer, and select one of the customer addresses as the billing and shipping address, which will display read-only details of each address.
To practice some more with this technique, we'll repeat similar steps for the credit card ID on the Payment tab. Instead of showing a text field to enter ID of a stored credit card, we will display a dropdown list with a choice of credit cards for the current customer person, and credit card details when one is selected.
First off, let's define some common credit card related types in the credit_card.xom file, and update the corresponding “credit card” object fields to use these types. We'll also change the base type for the "credit card" type to "integer enumeration", as displayed below.
Next, we'll open the person_credit_card.xom file, where the association between a person and their credit cards is defined, and will add a "read list" operation with the business entity ID as the input, and the credit card details as the output. We will also define a dynamic enumeration, and use a special “credit card name” parameter for the description.
Similar to what we've done before, we
will add
a customized CreditCardPaymentObject data object in the sales_order.xom file, as well as a generic "credit card info" structure for this object to add fields to it, as shown below.
We will also configure our data object to make credit card non-key fields read-only, and to make the credit card ID a trigger with a proper label.
Next we will update the "payment info" and "payment update" structures to push the credit card ID and the credit card approval code parameters
down
to a nested inline structure, that is also tied to that data object.
We'll also need to add the CreditCardPaymentObject as a child of the SalesOrderPaymentObject using the same name "credit card", as we used in the above structure.
Since the list of person's credit cards depends on the selected customer, which is in a sibling child object on the
s
ales
o
rder
data o
bject, we will implement the trigger in their common parent object SalesOrderObject, so we'll need to customize it.
Let's build the model project, and refactor our customer service code in the SalesOrderServiceExtended.cs to use the new nested structure in the GetPaymentInfo method as follows.
And similarly in the UpdatePayment like this.
Next we'll provide custom implementation to filter credit cards by the business entity in the PersonCreditCardService as follows.
And also the custom implementation to read all the credit card parameters, where we will return the credit card name as the card type, and the last four digits of the card number.
After that we will define the custom CreditCardLoader class inherited from the generated cache loader for our dynamic enumeration, which uses the person ID from its
salesOrder
object as the input for the operation to read the list of credit cards. It will also update the CreditCardIdProperty on the corresponding grandchild object of the salesOrder object whenever the customer changes.
After that we will just construct our PersonCreditCardLoader during initialization of the customized SalesOrderObject, similar to how we did it with the AddressLoader before, as follows.
In the customized CreditCardPaymentObject, we will add a credit card change listener, which will populate other properties from the credit card attributes. The expiration property will combine both expiration month and year, as shown below.
Once again, let's run the application, and review our changes. The Payment tab should now look as follows.
As you see, we're displaying a list of person's credit cards using the credit card type and the last four digits of the card number, and show specific credit card details when one is selected from the list.
3.8 View layout and master-details view
So far, the search and details views that we have been generating were using a default layout, with fields arranged in two columns flowing vertically, details views popping up as modal dialogs, and the first level child panels organized using tabs. Xomega allows you to configure some of the layout parameters for each view, and also share such configurations between multiple views. Let's open up global_config.xom file, and expand the
ui:layout-config
section to view the initial named layouts that were pre-configured in the model by default.
As you can see, we have a “standard” layout that is set as default for all views, with the configuration that we described above, as well as a collapsed “master-details” layout, which inherits from the standard layout, and overrides the
details-mode
for list screens.
In order to provide a layout configuration for a specific view, you need to add a
ui:layout
element to the corresponding
ui:view
element, set a named layout to inherit the configuration from in the base attribute, and/or override any specific layout settings inside of that element.
Let's demonstrate this by adding a layout configuration to the SalesOrderView. We will derive it from the standard layout, set the size for the view, and change the flow of the fields to be “horizontal” across columns as follows.
Next, we will regenerate the views for the sales order, which are the only artifacts affected by this change.
If you run the application now, and then open up sales order details screen and look closely, you'll notice that the fields are now flowing horizontally across the columns instead of vertically, as indicated below by the green zigzag lines.
You can also achieve the same by rearranging the order of corresponding parameters in the model. This layout setting doesn't work particularly well on this screen, so we will go ahead and revert it back to "vertical" in the model.
A more interesting thing to show would be how to change the Sales Order List screen to use master-details layout. As you may have guessed already, we will add the layout configuration derived from the “master-details” layout to the Sales Order List View as follows.
This change affects both Views and View Models, so we can either run both generators one after another, or just build the entire model to run them all. Here is what the screen will look like after you run the application.
While it was so easy to change the layout of the list screen to master-details, this type of layout creates a new set of challenges that did not manifest themselves when opening details as modal popup dialogs. Notice how the selected record that is shown in the details is also highlighted in the list. If you refresh the list now by clicking Search, or by saving the sales order, this selection will be cleared, since the framework doesn't have a unique field in the list to restore the selection. In order to correct that, we will customize the Sales Order List data object in the model,
and then run the “Xomega Data Objects” generator.
In the customized SalesOrderList data object,
we will set the generated SalesOrderIdProperty as the key column
, and the Xomega Framework will take care of restoring selection on refresh by using it as the unique ID for the rows.
4. Implementing security
Virtually any business application requires implementation of some level of security in order to authenticate the user, and to restrict application's functionality based on the user’s permissions. In this section we are going to show you how to add security to our demo application using Xomega and standard claims-based .Net security frameworks.
The Adventure Works data model that we use in our example doesn't really have a clear representation of a user entity with a unique user ID that can be used for sign in. There is a “person” object, which we can use as a surrogate for the user object, but it has an internal auto-generated integer ID, which we cannot really expect the users to provide for authentication. Therefore, we will use the e-mail address associated with the person as the user ID. We also have a “password” object, which stores hashed passwords and the salt, that can be used to authenticate the user. Highlighted below are the relevant files in the model.
Similarly, our data model doesn't have any tables that store user roles or privileges, but the “person” object has a "person type" field that we can use as a surrogate for the user role. Following is the description of the different person types, and their meaning.
As you see, it allows for both internal users, such as employees or sales persons, as well as external users that are associated with a particular vendor, store, or an individual customer. We will allow access to the application for all types of users, but the external users will need to be able to see only the data that is associated with their business entity, i.e. only their own sales orders.
4.1 Creating claims identity
Let's update our model and add some code that would allow us to construct a claims identity for a user. We will start by declaring a type for email in the email_address.xom file, which we're planning to use as the user ID.
Notice how we use a customized ASP.NET control for emails here. We will also use this type on the email field of the "email address" object.
Next, we will create enumeration "person type" with possible values of the person type and their descriptions, as well as declare a type with the same name for that enumeration, and will use that type on the "person type" field in person.xom.
With that done, let's declare a structure "person info" that will contain all the necessary information for the security claims as follows.
Finally, let's add an operation "read" to the “person” object, which will accept an email address as the input key, and output the “person info” structure that we created. Given that the setup for this operation is very custom, Xomega will not be able to generate a meaningful default implementation for this method. Instead of trying to inline any custom code into the generated service file like we did before, we will want to just subclass that generated service, and override the entire method in there. To enable that, we will add a
wcf:customize
element to the
config
section of the “person” object, and will set its
subclass
attribute to
true
. The following picture illustrates this setup.
Let's build the model, and navigate to the generated PersonService class under the AdventureWorks.Services.Entities project. Nested inside that class will be a file for the custom service implementation PersonServiceCustomized. Let's go ahead and implement the Read operation there as follows.
As you see, we are using several joins and some rather cumbersome left joins in LINQ to retrieve the data.
In order to encapsulate common security related code that can be shared between different types of projects, we will add a new static class called SecurityManager to the AdventureWorks.Services.Common project, where we will define a method that creates claims identity from a PersonInfo structure as follows.
For most of the fields in the PersonInfo structure we used standard claim types. For the person type we used the Role claim type, which is consistent with how we're using it in the first place. For any non-standard info, such as the store ID or the vendor ID for the user, we're using custom claim types, which we declared as constants at the top of the class.
To simplify any security checks in the code, we can also define some handy extension methods on the IPrincipal interface. For example, the following methods will allow us to easily check some user roles, while also leveraging constants that were generated from the "person type" enumeration that we defined in the model.
Here's another example, where we can easily retrieve typed values of the custom claims, such as the store ID associated with the user.
Now that we have done all the prep work, we can move on to implementing user authentication.
4.2 Adding OWIN authentication
In this section we are going to implement the Login View for our application, as well as a service that performs user authentication by email and password. As usual, we will begin with defining the relevant things in our Xomega model. Let's open up person.xom file, and declare a new data object called AuthenticationObject, which will serve as a data model for our login view.
Next we will define a new structure "credentials" with two parameters for the email and password, which will be added to our AuthenticationObject as properties.
Notice how we use the “email” type that we defined earlier, and a "plain password" type for the password parameter, which was pre-configured in the Xomega model. Next let's add an operation "authenticate" to the person object, which will use this structure as an input argument.
Even though the operation is not going to update anything in the database, we marked it with type "update", so that the Login View would be generated with a Save button and the logic to call this operation with the supplied credentials, which is pretty much what we want for the Login button. So let's go ahead, and add the actual Login View definition in the model as follows.
Let's have a look at some things that we have configured on our Login View. We will need to customize the logic for the generated view to implement the actual OWIN cookie-based authentication in our application, so we marked it with the
customize
attribute. We set the
child
attribute to
true
, so that this view would not be added to the main menu by the generator. We set our AuthenticationObject as the view model for the view. We used the standard layout for the view, but we overrode it to make the fields stack up as one column instead of two, and to also give it some size.
These are all the changes that we need in the model, so we can go ahead and build the model project now. Once the model build finishes, let's open the PersonServiceCustomized.cs under the generated PersonService.cs, and provide a custom implementation for the Authenticate method as shown below.
This is where we would need to hash the user supplied password using the salt that is stored in the database along with the hashed password, and compare the result with the latter. For ease of testing though, we will instead just compare it with a hardcoded word "password" for now.
Now the time has come to flesh out any customizations for the Login View. This is another example of how Xomega allows for customizations on different levels. When customizing a view in the code like that, you will have access to all the generated controls, so you can programmatically update them however you need.
Let's expand the generated Login View under the web client project, and open the nested LoginViewCustomized.ascx.cs file. We will start by adding an override for its OnInit method, where we will change the text of the Save button to be "Login", update the width of the main panel, and handle the case when an already authenticated user was redirected to this view if they were not authorized to access the view they were trying to open. Here's a picture that illustrates this.
As we mentioned before, the generated code for the Save button, as well as the Xomega Framework base classes, will automatically handle both the UI validation of the supplied email and password, and the invocation of our Authenticate service method to check them against the saved password. If it succeeds, then the view will fire a ViewEvent about it being saved, which we will want to leverage to construct a claims identity for the user, and authenticate the user within the OWIN context. Here's what the corresponding method in our customized view will look like.
You can see how we call our Read operation on the PersonService using dependency injection, and leverage the CreateIdentity method on our SecurityManager class that we created earlier. After we sign in this identity with the current OWIN context, we redirect to a URL that is supplied in the query string, if any.
All we have left to do now is to make some configuration changes for our web application. Let's open up the Web.config that is located under the Views folder of the web project, which is where all the views are located. We will change the authorization for all views to deny access to non-authenticated users, except for the Person/LoginViewPage.aspx, which will allow access to all users, as follows.
Make sure that you make this update in the Web.config under the Views folder, and not at the root of the web project. Otherwise, other resources such as java scripts or style sheets will be inaccessible from the browser, and your application will not function correctly.
Finally, let's update the Web.config, this time in the root of the web project, to map the Login path to our login page as follows.
Alternatively, you can change the LoginPath parameter in the Startup.Auth.cs file to point to the login page.
Let's run the application and check out the results of our hard work. You will notice that the application presents a login form now, where you can enter an email and a masked password. If you enter invalid credentials, you will get the message from our service.
When correct credentials are entered, you will see the home screen, and your user full name will be shown in the upper right corner in the title bar.
Next to the username you will see a Log Out link, which allows you to log out, and takes you to the login screen again. This completes our implementation for the authentication part. In the following sections we will show you how to implement authorization to secure different parts of the system based on the user permissions.
4.3 Securing business services
Following best security practices, you'd always want to secure business services first, to make sure that the user cannot call any operations that they are not allowed to call, and that they don't have access to any data that they are not allowed to see.
In our example application, access to sales orders should be allowed only to internal employees, and to individual or store customers, but not to vendors or other types of users. Moreover, external customers should be able to see only their own sales orders. Let's see how we can implement these security requirements within the business services generated by Xomega.
Let's open ReadList method of our SalesOrderService implementation class, and add the following custom code for security checks at the top of the method.
Notice how we are using the CurrentPrincipal member of the service to determine permissions of the current user, and leverage the convenient methods that we added to the SecurityManager class earlier for that. If security check fails, we report a critical error of type Security, which will abort the execution, and return that error to the client.
Now, let's go down in that method, and find a custom code placeholder for additional filter criteria on the source query. Here we will add some custom code that checks if the CurrentPrincipal is a store or individual customer, and then add their associated store ID or person ID respectively as additional filter criteria for the sales order's customer.
As before, we are using our custom extension methods to easily retrieve the store ID or person ID for the CurrentPrincipal.
In order to test our security logic, we will run the application, and enter email address "
jay1@adventure-works.com
" as the user ID, which belongs to a “store customer” type of user. If we then open a Sales Order List screen, and hit Search without specifying any criteria, we will see the following results.
From that screen you can see that despite providing no search criteria, it only displays six orders, and the customer name on all of them matches the name of the currently logged in user.
4.4 Securing UI views
While securing business services can generally fulfill your security requirements, you typically also want to hide any UI fields that the user should have no access to, or disabled those that they have read-only access to.
Our Sales Order List screen has criteria by customer store and name as shown below.
However, they don't make much sense for external customer users, since the list will always show only their own sales orders, as we have coded in the previous section. In order to hide these fields for external customers, let's open our customized SalesOrderCriteria data object, and add the following code to its OnInitialized method.
In this case, we are using the CurrentPrincipal member of the current thread, as well as our handy extension methods to determine the user privileges. For external customers we set the AccessLevel to None on the operator properties for customer store and name, and Xomega Framework takes care of hiding the property-bound controls, and their labels for us. Note that with this logic being in a data object class, it will be also reusable with WPF desktop clients.
If we run the application now, and log in as our external user, we will see that these fields are no longer shown on the screen.
Finally, you may want to disable access to the entire screen for certain types of users. As we discussed before, access to the Sales Order List screen should be only to internal employees and external customers, but not to vendors or other types of users.
Within the ASP.NET web framework this is achieved by setting up authorization in the Views/Web.config file for specific paths (or initial parts thereof), as shown below.
This will also automatically hide the corresponding menu options for the users, since it has security trimming enabled. If the user manually types in the path to a prohibited screen in the browser though, they will be redirected to the login view, which will display the Unauthorized message that we configured in our custom code.
5. Adding a WPF desktop client
So far we have shown you how Xomega allows you to model, generate and customize a powerful full-fledged web application based on the classic ASP.NET framework. Now let's see how we can do the same for a desktop client using WPF framework.
Remember how we selected Xomega ASP.NET Web Solution as the first step for building the web application. If we were to build a desktop application from scratch, we would've selected Xomega Client-Server WPF Solution or Xomega Multi-Tier WPF Solution as the first step, and then repeated most of the same steps as before.
In this section, by contrast, we will show you how easy it is to add a new desktop client to an existing Xomega web solution, and reuse most of the hard work that we have put into it. This way, you can also run both solutions side by side.
We will start with a simpler client-server architecture, where the desktop application communicates directly with the database through the existing shared service layer. After that we will show you how to host our service layer using WCF, and change the desktop application to use multi-tier distributed architecture with those WCF services as the middle tier.
5.1 Adding client-server WPF project
Let's open our existing Xomega ASP.NET web solution for the AdventureWorks demo application that we have built so far. Select a menu to add a new project to the solution, and pick a Xomega WPF Client template under the Presentation Layer folder. We will add it to the AdventureWorks folder next to all other projects in the solution, and we will give it a name that follows Xomega conventions, so that it would work with the preconfigured generator settings, and would therefore require minimum changes.
Next, let's open up properties of the “WPF Views” generator in the model project, and change
Include In Build
property to
True
, so that the views are always regenerated when the model is built. You can also update any paths here as needed, if you didn't follow Xomega conventions when creating the WPF project.
Before we run it though, let's make a certain update to the model, which will also demonstrate how to customize WPF controls in the generated forms. Let's recall how we customized the "sales person" logical type
in the
sales_person.xom file in section 2.6. On the one hand, it was a key type, so it was important to keep it inherited from the "employee" key type instead of the "integer enumeration", in order to preserve the relationship between the "sales person" and "employee" objects. On the other hand, we wanted it to have the property and web control configurations from the "integer enumeration" type, since we turned it into a dynamic enumeration, so we copied the corresponding config sections over to our type. Now we will do the same for the WPF control configurations, so that it would use a combo box or a list box as the editor, as shown below.
We don't need to build the entire model at this point, but rather just to run the “WPF Views” generator, so let's go ahead and do it.
A WPF Client project template, when added to an existing solution, is configured to work with a WCF middle tier, as opposed to using a client-server architecture. Therefore, we will need to make a few changes to reconfigure it for the client-server environment.
First, let's add to it a reference to our AdventureWorks.Services.Entities project, which contains implementations of the back-end business services that communicate with the database. Specific service implementations will be abstracted from the client code by dependency injection, but we need to update the initial service container configuration to use those concrete implementations. For that, we will open the WpfAppInit.cs under the App_Start folder of the WPF project, comment out usage of WCF Services, and add the configuration for concrete services from the Entities project that we just referenced, as follows.
After that, let's open the application class in the App.xaml.cs, and remove the line that performs default WCF authentication, as illustrated below.
Since the AdventureWorks.Services.Entities project that we referenced depends on Entity Framework package, we will need to open a NuGet Package Manager for our solution, and add Entity Framework to the WPF project as well.
Finally, we will need to copy connection string settings from the Web.config of our web project to the App.config file of our WPF project.
If we set the WPF project as the startup project for the solution, and run the application now, we will get a default main window with a main menu for our views, as shown in the following picture.
Let's select the menu option to create a New Sales Order. An empty Sales Order Details screen will pop up, with the same fields and structure as in the web application, as shown below.
All the logic for customer look up, contextual selection of the address type and the credit card that we put into our customized data objects and so on, is reused in the WPF screens as well.
As you see, we have created full featured WPF forms using Xomega with virtually no coding. The sales order list screen is backed by our secure service, and will give us the "Operation not allowed" error when we run the search, since we haven't implemented authentication yet. That’s what we'll do in the next section.
5.2 Securing client-server application
To complete development of our client-server WPF application, let's implement user authentication using the Login View that was generated for us from the model. We will begin by opening the application file App.xaml.cs, and then removing the call to start the Main View on the application startup, and adding code to pop up the Login View instead, as follows.
Next, let's open the code for our
customized WPF login view
in the LoginViewCustomized.cs file, which will be nested under the generated LoginView.xaml, and update the text of the Save button in the OnInitialized method to be Login.
As with the web login view, we can override a method to handle the "View Saved" event in order to construct the claims identity for the current user. However, we will do it differently here by overriding the actual handler for the Save button, as shown below.
We can still call the Save method on the DetailsViewModel (dvm) here, which will take care of calling our Authenticate operation with the supplied email and password, and then return if there are any errors. If authentication was successful, we will read the person info, and construct a claims identity for the CurrentPrincipal using our shared method on the SecurityManager, similar to the way we did it in the web. After that, we start the main view (using the call that we removed from the application startup), and then close the current login view. It is important to set the CurrentPrincipal before that to ensure that it will be set on the GUI thread.
If we run the application now, up pops our login dialog, where you need to enter your user credentials.
If you supply valid credentials, you will be taken to the Main View, from where you can launch the Sales Order List screen. Now that you're authenticated, you can run the search, and click on specific sales orders, which will be displayed using the master-details layout that we specified in the model.
5.3 Adding WCF middle tier
In the previous sections we saw how to add a WPF client to our application, which communicates directly with the database. While this client-server architecture is quite simple to implement, it may not work well if the desktop client is used by many users. You have to supply the database credentials in the App.config for every client installation, have all the users update their connection strings whenever the database is moved to another server, e.g. during failover or disaster recovery, and reinstall the application on each desktop for any changes in the business logic.
Enter multi-tier distributed architecture
. With the business layer hosted on a separate middle tier, you can update the connection strings, or redeploy new services with updated business logic without having to upgrade any individual desktop clients, as long as the service interfaces are still compatible. Let's see how we can add the middle tier with our business services that is implemented using WCF framework.
We will start by adding a new project to our solution, and selecting the Xomega WCF Web Services project template under the Service Layer folder. As with the WPF project, we will add it to the solution folder next to all other projects, and we'll give it a name that follows Xomega conventions in order to minimize the configuration changes required.
Once the project is added to the solution, let's open Web.config file in the new project, and move the database connection strings to it from the App.config file of the WPF project.
Now let's set the
Include In Build
parameter to
True
on the following generators: "WCF
Service
Host Files" and "WCF Server Configuration" under the Service Layer folder, and "WCF Client Configuration" under the Presentation Layer\WPF folder, as shown below.
In order to expose our business services through WCF, we need to decorate our service and data contracts with proper WCF attributes. To do that we’ll open properties of the Service Contracts generator, and set the
Wcf
parameter to
True
, as follows.
We will also need to make sure that the AdventureWorks.Services.Common project references the required framework libraries System.Runtime.Serialization and System.ServiceModel.
At this point let's build our model to regenerate all the artifacts, and then update some code in the WPF project. First, we will change the override for the Save button handler in our customized Login View class to replace the previous client-server implementation for authentication as follows.
As you see, we no longer delegate it to the view model's Save method, which invokes the Authenticate operation, nor do we read the person info to construct the claims identity for the CurrentPrincipal. Instead, we validate the authentication data object first, to make sure that all required fields are populated, and then call WcfServices.Authenticate utility method (that we removed earlier from the App.xaml.cs), which acquires a security token from the WCF services for the specified credentials, and constructs a claims principle from that token. This token will be used for any subsequent communication with the WCF services in a secure manner. Also note, that we turn off modification tracking of the authentication data object, in order to suppress a confirmation dialog about unsaved changes when closing the login view.
Finally, let's open the WpfAppInit.cs file, and change the service container configuration to use WCF services, rather than the concrete service implementations directly. At this point, we can even remove the reference to the AdventureWorks.Services.Entities project from the WPF project.
In order to run the application from the Visual Studio, we need to set both WPF and WCF projects as the startup projects for the solution, as shown below.
Also, let's open properties of the WCF web project, and configure the Start Action to not open any pages, as shown below.
By default WCF services are setup to use a certificate for "localhost", such as the IIS Express Development Certificate that can be installed by Visual Studio when you enable SSL. Make sure that you have such a certificate installed, or use the C:\Program Files (x86)\IIS Express\IisExpressAdminCmd.exe setupsslUrl command to set it up.
You should be able to run the application now,
which will show you the login dialog
, and then other screens. If you get a prompt about untrusted certificates, select the option to trust it. However, since we have not implemented security in the WCF middle tier, which was preconfigured for anonymous logon by default, you will be able to use any credentials to log in, and the Sales Order List screen will always give you a security error if you try to run the search. So let's see how we can properly secure our WCF services.
5.4 Securing WCF services
The Xomega WCF Services project that we've added contains its own Security Token Service (STS) that is used to issue security tokens to the clients, which will be trusted by the rest of the WCF services. The STS code is organized in its own folder in the project, as illustrated below.
If you want to implement single sign-on (SSO), you can configure the STS to trust security tokens issued by an external identity provider, and then just enhance those security tokens with application specific claims, such as the person type, associated store or vendor ID, etc. This would have to be done in the AppSts.cs file regardless of whether the application uses SSO, or performs its own authentication.
Given that we use simple username and password authentication in our sample application, we will need to add our authentication logic to the UserNameValidator class, which was provided by default with the project template with a TODO to insert your code. The following picture demonstrates how to add user validation in there using our Authenticate method on the PersonService.
Now let's update the AppSts class to return application-specific claims identity for authenticated users. We will remove the default implementation that only sets the username, and will replace it with the familiar calls to read the person info from the PersonService, and construct a claims identity from it using our utility method on the SecurityManager class as follows.
Let's run the application now, and log in as an external customer. If you open the Sales Order List screen, you will notice that the customer criteria are hidden, since it uses the existing data object as its model, where we had added security checks before. If you hit Search now without providing any criteria, you will see that it returns only the sales orders for that customer, as per the security implementation of that business service.
The initial setup of the WCF services helps you to get started quickly, but for the production release you will want to make certain changes, which are beyond the scope of this tutorial, such as the following:
-
Use SSL for the communication protocol. This will require updating WCF endpoint configurations in the corresponding App.config, Web.config and global_config.xom files.
-
Use your own certificate instead of the one provided with the project template. You will want to install it in the certificates store, update STS code to read it from there, and use the new certificate thumbprint in your Web.config.
-
Use SSO, which will require changing security configuration to trust the external identity provider, and switching to the federated authentication.
6. Adding a SPA web client
In the previous chapters, we have seen how Xomega helps you to model and build a web application using classic ASP.NET web forms, which generate the entire page on the server side. We also saw how to easily add a WPF desktop client to it - both as a client-server and multi-tier applications, - and reuse most of the code from the web application.
In this chapter we are going to show you how to create a different type of web client - Single Page Application (SPA) using modern architecture with REST services as the backend. The REST services will be implemented using Web API, and will wrap our existing business services for reusability.
The web client will be served as static HTML, JavaScript and other resource files, and all the client behavior will be implemented in JavaScript, which will be executed by the user's browser. To develop that JavaScript, we will actually use TypeScript from Microsoft that provides type safety and compilation checks. We will also use our XomegaJS library - a TypeScript counterpart of the Xomega Framework, - as well as other popular client-side libraries and frameworks, such as Knockout, JQuery, Durandal, etc.
As was the case with the WPF client, if we were to build a SPA web client from scratch, we would select the Xomega SPA Web Solution template for our solution at the start, which would create all properly configured projects for us. After that we would repeat the same steps as we did for the ASP.NET web solution, except that any client code customizations would be done In TypeScript within the XomegaJS framework instead of the C# based Xomega Framework.
To leverage all the work that we have already done though, we will just add additional projects to our existing solution, and will update them as needed to build the SPA application. This will also show you how you can run different types of services and clients side-by-side, while reusing the same model, services, and other shared code.
6.1 Adding REST services
As we mentioned before, the SPA client will be using REST services as the backend, so we will start by adding a new project for those. We will select Xomega REST Web Services project template under the Service Layer folder, will create it in the same folder as the other projects in the solution, and we'll give it a standard name, following Xomega conventions to minimize any configuration changes, as shown below.
Next, let's open properties for the “Web API Controllers” generator under the Service Layer folder, and set the
Include In Build
parameter to
True
.
This generator will create Web API controllers for any operations in the model that are annotated with a
rest:method
attribute. If we had created a SPA solution from scratch, it would've been preconfigured to generate such rest attributes whenever we were enhancing the model with new operations. However, because it was not set by default for the classic Xomega ASP.NET web solution, running this generator now will not produce much, until we add such attributes to our operations as needed.
First off, let's go ahead and correct the configuration of the generators that add model operations for us, by setting the
Generate Rest Methods
parameter to
True
on them. We will update the following generators under the Model Enhancement folder: "Full CRUD with Views", "Enumeration Read List" and "Lookup View", as illustrated below for the first generator.
This will ensure that the Rest methods will be generated going forward, but we still need to add them to the existing operations. To help with this process, we will clone an existing generator under the Model Enhancement folder, such as "Enumeration Read List", by pressing Ctrl+C on it, and then will rename it to "Rest Methods" and set every boolean parameter to False, except for the
Generate Rest Methods
, which we'll set to
True
, as shown below.
Now we can
run this generator on all the files that we have added operations to
, and it should only add the Rest methods without any other changes. However, it would still be a good idea to
commit your model now
to a source control, so that you could review the diffs after you run it, and easily spot any extraneous changes. We can select
files
product.xom, ship_method.xom, customer.xom, sales_order.xom, sales_person.xom, sales_reason.xom, sales_territory.xom
and
special_offer.xom
in the project explorer one by one using Ctrl-click, and then run the "Rest Methods" generator on them, as shown below.
While we're at it, let’s open the "sales person" type in the model, and specify the TypeScript module and class for its Data Property, as well as the HTML controls to be used as the editor for this type, as follows.
For the files were we added operations for contextual enumerations, we will just add the Rest method manually. Here is what we will add in the "read list" operation of the
business_entity_address.xom
:
Notice how we use the operation’s input parameter in curly braces as part of the URI template. We'll make a similar change in the
person_credit_card.xom
.
The only other file with operations that remains in our model is
person.xom
. However, it only has security-related operations, which will be used internally by the REST services for authentication, so we don't need to expose them via REST endpoints. If you run the “Web API Controllers” generator now, you should see the controllers generated for all our REST operations grouped by module.
Our REST services are hosted using OWIN middleware, and configured to use JWT authentication and authorization. By default it authenticates any user, including anonymous users as Guest. We will need to update it in the file AppAuthProvider.cs under the App_Start folder to use our authentication service, and to construct proper claims identity as follows.
As usual, we are reusing all our security related services and utility methods here. We also need to copy the connection string settings to the Web.cofig file of the REST services project.
Finally, let's open properties of the REST services project, and set the Start Action to not open any page, just like we did with the WCF services.
Now that we have securely exposed our business services via REST, let's see how we can add a SPA client on top of these services.
6.2 Adding SPA client project
As we mentioned before, Xomega SPA client is implemented using TypeScript. So first of all, you need to make sure that you have
TypeScript for Visual Studio 2015
installed (current version is 2.3 as of this writing).
Similar to what we've done with other projects, let's add a new project to the solution, and pick the Xomega SPA Web Client project template under the Presentation Layer folder. As before, we will follow Xomega conventions for the project name to minimize configuration changes, and will create it in the solution folder next to all other projects.
Next, we will set the
Include In Build
parameter to
True
on all generators in the SPA folder, as illustrated below.
Let's build the model project now to rerun all generators. This should add generated artifacts, such as views and data objects, to our new Spa project. Let's expand the generated Login View, and open the LoginViewCustomized.ts nested under it. Here we can customize our view in the attached method using JQuery, as follows.
As you see, we changed the text of the Save button to be Login, set the width of the view, and configured the authentication data object to not track modifications, in order to suppress the confirmation dialog when navigating away from it.
Now let's open the
Login class
in the login.ts file that was provided with the project by default, and make it extend from our Login View. After that, we need to remove the empty view it creates, as well as its anonymous auto-login logic, and add an override for the onSave method instead, where we will use supplied email and password to log in, as follows.
To run the SPA client, we will open the solution properties, and set the SPA client and REST services projects as the startup projects. The former does not need to be started in debug mode, since we will use the browser's debugger for our client-side TypeScript/JavaScript code.
If we run the project now, the browser will show our Login View, where we will enter credentials of an external customer.
Once you login, and navigate to the Sales Order List, you will notice that this SPA client looks very similar to the ASP.NET web application that we have built initially. It has small differences, such as the results grid having a page size selector, and showing the number of records - both on the current page and total, - but overall it will have the same master-details layout with the save fields, columns, and actions as the ASP.NET client.
You will notice, however, that it does not have any custom client-side logic that we added to the ASP.NET client, such as hiding the customer store and name criteria for external customer users, or displaying contextual data for the billing and shipping address of the selected customer on a sales order.
All this logic was implemented in C# using Xomega Framework, and was successfully reused with the WPF client. But since the SPA client is written in TypeScript, we will need to re-implement this logic using XomegaJS framework instead. Luckily XomegaJS makes it very easy, and very similar to what we did using Xomega Framework, as we will see in the following sections.
6.3 Securing SPA views
We have already implemented authentication in our SPA application, so let's see how we can add authorization logic on the client, both for hiding or disabling certain fields based on the user's role, and for restricting access to the entire views.
To hide the customer store and name criteria for external customer users, let's open the
SalesOrderCriteriaCustomized.ts
file, and set the access level on the corresponding properties to None, if the current user's role is a store contact or an individual customer, as follows.
Notice how we can still use generated constants for the “person type” enumeration in TypeScript. If we run the application now, and log in as an external customer, we will see that those fields are hidden on the sales order criteria, as shown below.
Next, let's make sure that sales related views are only accessible to internal users and external customers, but not to other types of users, such as vendors. We'll do it by configuring rules for Durandal routes in the shell.ts file that is nested under the shell.html as follows.
Here we just check if the route contains the word 'Sales', but you can check for specific routes, or apply any other logic as needed. If you log in as a vendor user now (e.g. jon2@adventure-works.com), you will see that the screens to search existing or create new sales orders are no longer shown under the Sales menu.
If you try to manually enter the URL for those views, or just pull them up from a saved favorite, you will see that the views will not be shown unless your user has a proper role.
6.4 Using auto-complete in SPA
In section 2.7 we turned the status criteria for sales orders into an auto-complete text field in the ASP.NET application. Using the same example, let's see how we can do it for the SPA client as well. If you recall, we configured the "sales order status" type in the model to use the ASP.NET TextBox control for that. Now we will need to do the same with the HTML input control as follows.
Let's run the "SPA Views" generator now to regenerate the Sales Order List view. After that, we will open our customized SalesOrderCriteria object, and will set the display format for this status property to be "ID - Text", just like we did in C#.
Notice how we use the Header class from the XomegaJS to reference parts of the value when constructing the display format. When you run the application now, the status criteria should behave exactly the same way as it did in the ASP.NET application.
6.5 Configuring cascading selection in SPA
If you recall, we added some client-side code in C# to make the list of sales territories on the SalesOrderCriteria object depend on the value of the selected global region. What can do the same just as easily in TypeScript for the SPA client, as follows.
Here we also use generated constants for attributes on the "sales territory" enumeration. Once you do that, and run the SPA, you should see the list of possible sales territories update whenever you change the global region criteria.
6.6 Custom UI validations in SPA
As it is the case with the C# data objects, their TypeScript counterparts will automatically validate their properties for required values or invalid format, and report the errors on the screen. To add custom validations, such as cross-field validation for the From and To values of the Order Date criteria, we can override the
validate
function in our customized SalesOrderCriteria object, as shown below.
If you run the application, and enter invalid values in the Order Date criteria, you should see our custom error message displayed.
6.7 Adding contextual selection in SPA
The generated Sales Order details screen in our SPA client has a proper structure based on our Xomega model, but it fails to display customer address details for the selected or current customer, since that was implemented using custom client-side logic in C#. In this section we are going to show you how to do the same in TypeScript for the SPA client. This logic is a little more complicated than our previous customizations, but leveraging XomegaJS framework, as well as all the different pieces that were pre-generated for us, makes it still fairly easy and straightforward.
Similar to the way we did it in C#, we will open the SalesOrderCustomerObjectCustomized.ts file, and add to it a new class CustomerAddressLoader that extends from the generated cache loader for the business entity address. The class will take our data object in the constructor, and will subscribe to the changes in the
StoreId
and
PersonId
properties. When those change, it will read the new list of addresses for the current store or person using the logic from the base class, except that it will override its
loadRequest
method to use the proper Id for the ReadList operation. When the new list is loaded, it will update the address on the billing and shipment address child objects. Then we just need to create this class during initialization of our customized SalesOrderCustomerObject. The following code snippet illustrates this logic.
Notice that we also turn off modification tracking on the child LookupObject, since its values are only used to conveniently look up customers, and should not prompt for unsaved changes per se.
Next, you will need to add custom code in the AddressObjectCustomized.ts file, which would update the AddressObject's properties from the attributes of the selected address value whenever the latter is changed. Below is the code that shows how to do it.
Let's run the application now, and open up the customer tab on a new or existing sales order. If we select a customer that has more than one address types, we should see those types populated in the billing and shipping address panels. Once you select a specific address, it's details will be shown in the rest of the panel.
As an exercise, let's repeat the same process to populate the list of credit cards for the selected customer, and to display the credit card details when one is selected. Since the selected customer trigger and the credit card details are defined on different child objects of the SalesOrderObject, we will implement this custom logic in their common parent object in the SalesOrderObjectCustomized.ts file. We will define the PersonCreditCardLoader class, which extends from our generated cache loader for person's credit cards, takes the SalesOrderObject in the constructor, and configures it using the logic that is similar to that for the customer addresses, and also similar to the one we had developed in C#. Here is what this file will look like.
Again, to display the details of the selected credit card, we will add the following logic to the CreditCardPaymentObjectCustomized.ts file.
Now let's open the sales order details screen, make sure we have a customer selected, and then select the Payment tab. We should see the list of saved credit cards for the selected customer, which pretty much always consists of just one credit card in the demo database. Selecting the credit card from the list will display the credit card number and expiration, as shown in the following screen.
Next steps
In this tutorial you have seen how to use Xomega model driven development platform to quickly and easily build full fledged desktop and web applications using a range of technologies from the classic ASP.NET framework to the modern Single Page Application approach.
We have covered a great number of typical use cases and scenarios for advanced search and details views, which should help you get started with your own projects. Remember that the source for the example Adventure Works applications that we have built in this tutorial are available on
GitHub
, and you can always run it, or use it as a reference.
You saw how flexible and customizable Xomega platform is. But it is also extremely extensible, allowing you to add custom model elements, modify existing generators, and even write your own generators to produce an architecture that is tailored to your own needs.
As you start using Xomega in your own projects, you can look at our How-To guides, as well as ask questions on our forum. Both Xomega Framework and XomegaJS are open source projects hosted on GitHub, so you can troubleshoot any issues with them using the original source code. We also welcome your help and contributions to these projects, such as documentation, bug fixes, or ideas for new features. And, of course, we would love to hear your feedback on our technology, framework and tutorials, so that we could make it even better.