Saving to Multiple Data Sources in the HTML Client (Stephen Provine)

Saving to Multiple Data Sources in the HTML Client (Stephen Provine)

Rate This
  • Comments 4

One of the great features of Visual Studio LightSwitch and Cloud Business Apps (CBA) is its ability to perform what we call data mashup. You can connect to multiple data sources, configure virtual relationships between entities in different data sources, and then visualize all of the data at the same time.

One interesting data mashup scenario is to attach to an external entity, then extend that entity with some application-specific intrinsic (internal) data. Perhaps the external entity represents basic Employee information but the application is a corporate Facebook-like application which would like to additionally store more detailed profile information such as a photo and areas of expertise.

In this scenario, it is possible that application users would want to view and edit all the employee information together. LightSwitch easily supports mashing up the external and intrinsic entities into a single view, but editing is problematic, as changing data on both the external and intrinsic entities at the same time results in pending changes to both data sources, and these changes cannot be saved in a transactional manner.

Because of this problem, LightSwitch does not automatically enable saving to multiple data sources. However, there is a code entry point through which an application developer can write logic that chooses precisely how to handle such a scenario. This post walks through an example of how to effectively make use of this entry point when the situation calls for it.

Example

The example application we are going to create takes an existing data source that contains some basic person information, then extends it with additional profile information stored in the intrinsic application database. We will then build a HTML client that can both view and edit all of the person information in a single set of browse, view and edit screens.

This post assumes some previous knowledge of LightSwitch, including how to create LightSwitch projects, attach to existing data, navigate the entity designer and create screens using the built-in templates. Also, this example assumes that you are using Visual Studio 2013 Update 1 along with the March 2014 Update of the Office Developer Tools (which includes LightSwitch).

Creating the Project and Attaching to Person Data

Start by creating a new LightSwitch HTML application called “ProfileApplication”. After the project has been created, we want to attach to the existing person data source. For this post, let’s use an example read/write OData service located at http://www.odata.org/odata-services. Open this link, choose the “OData v3” tab and click on the “OData (read/write)” link. This will navigate you to a temporary URL assigned by the OData web site that allows for both reading and writing data. Copy this URL to the clipboard – it will look something like “http://services.odata.org/V3/(S(code))/OData/OData.svc/”.

Now in your LightSwitch project, attach to this OData service, making sure to uncheck the “Attach to this data source as read-only” option and picking “None” as the authentication type. Choose to include the “Person” entity. After you click “Finish”, ignore the warnings.

Defining Person Profile Data

To extend the existing person data with additional profile information, add a new table and name the entity “PersonProfile”. We want to relate instances of this entity to the existing Person entity, so let’s configure a virtual relationship between them as follows:

image

Let’s also configure a second virtual relationship that connects a person with their manager. First add a property to the person profile entity called “Manager_ID” of type Integer that is not required, then configure a new virtual relationship as follows:

image

Now let’s add some profile specific properties to the entity. The person profile entity should now look something like this:

image

Finally, let’s do a little customization of the defaults in preparation for generating some screens. Switch the entity designer to the HTMLClient perspective, and for each of the “Person”, “Person_ID” and “Manager_ID” properties, uncheck the “Display by Default” option in the properties window. Also, open the Person entity from the attached data source and make the same change to not display the “ID” property by default.

Creating Screens

With our data model in place, we’re now ready to create some screens over the data. In the HTML Client project, open the add new screen dialog and choose the “Common Screen Set” template. Pick “DemoServiceData.Persons” as the screen data and hit OK.

Let’s do one quick customization of the view and edit screens to add the Manager property from the person profile entity. This was not added by the common screen set template because it navigates more than one level deep in the entity graph. In the view screen, add a new item under “left” using the “Other Screen Data…” option, and enter “PersonProfile.Manager” as the screen data path. Then move the newly created item so it is between Department and Skills. Now in the edit screen, add a new item, also under “left”, configured in the same manner.

image

Now hit F5 to run the application and see what we have. You are first presented with the browse screen over the persons on the external data source:

image

Clicking on a person opens the view screen, which includes not only the “Name” property from the external data source but also all the person profile properties:

image

These profile properties are currently empty, of course, as we haven’t added any data yet. You also see “Created By”, “Created”, “Modified By” and “Modified” properties which are maybe confusing since these properties apply to the profile information entity only. Feel free to rename these properties to something that is clearer.

Click the Edit button and you’ll see the add/edit screen, but notice that you can only make changes to the name of the person:

image

All the other properties are read only because a person profile entity has not yet been created and associated with the person. To provide a seamless experience for the end user, we want to make sure that person profiles are created on demand when they are needed. In this case, that would be when the add/edit screen for a person is opened.

Let’s add this logic to the add/edit screen. Close the running application and return to the designer for the add/edit screen. Using the “Write Code” link in the designer’s navigation bar, choose to write code for the “created” event. Add the following code:

myapp.AddEditPerson.created = function (screen) {
    if (typeof screen.Person.ID !== "number") {
        myapp.activeDataWorkspace.DemoServiceData.Persons
            .orderBy("ID").execute().then(function (persons) {
                var lastPerson = persons.results[persons.results.length - 1];
                screen.Person.ID = lastPerson.ID + 1;
            });
    }
    screen.Person.getPersonProfile().then(function (profile) {
        if (!profile) {
            var profile = new myapp.PersonProfile();
            profile.Person = screen.Person;
        }
    });
};

The first part of this code handles a deficiency in the sample OData service we are using, which is that it does not offer auto-increment ID values. This code is not related to the purpose of this post so we won’t focus on it. The second part of the code gets the profile associated with the person entity, and if the profile is missing, it creates a new one and associates it to the person entity.

Run the application again and you will see that the add/edit screen is now fully editable. However, it is not fully functional. If you simply click to save or you only modify profile properties, it succeeds because only the intrinsic database needs to be updated. But if you try to change both the name and some profile properties at the same time, you will get the error “Unable to save changes to multiple data sources”. Let’s see what we can do to fix this.

Saving to Multiple Data Sources

We are finally ready to illustrate the feature highlighted by this article! In Visual Studio, locate the code file where you wrote your add/edit screen created code. Underneath this code, add the following:

myapp.onsavechanges = function (e) {
    var personData = myapp.activeDataWorkspace.DemoServiceData;
    e.detail.promise = personData.saveChanges()
        .then(function () {
            var profileData = myapp.activeDataWorkspace.ApplicationData;
            return profileData.saveChanges();
        });
};

Let’s talk about what this code is doing. First of all, it’s handling a global event, and there can be only one handler. We added it to the code behind the add/edit screen in this example because it is the one place in the application where save occurs across multiple data sources. In a real application, you likely want to add this code in a more global location, perhaps in the code behind your home screen. And just to be clear, this code will work globally (as in across all screens), since the saveChanges method does no work if there are no changes to the data source in question.

Second, this code institutes a particular semantic around how the save occurs. First the external data source is saved, then the intrinsic database is saved, but only if the external data source was saved successfully. All this is done through promise objects, where the value of e.detail.promise is set to the final promise representing both save operations. By providing the promise back to the runtime, LightSwitch can make sure to wait until both data sources are saved before giving control back to the user.

Now if you run the application, you will find that when you edit a person, the application will first ensure a profile is created and then allow updating both the name and the profile information at the same time.

Handling Transaction Inconsistencies

Let’s consider what this code implies in terms of transactional consistency (or more specifically, lack of it). Notice that data could be saved to the external data source successfully, but it could fail to save profile information to the intrinsic database. To prove this is the case, let’s add a server-side validation rule on the profile information that causes the save to fail.

In Visual Studio, right click the “ApplicationData” node in your Server project and choose “View Code”. In this code file, add the following partial method:

partial void PersonProfiles_CanUpdate(ref bool result)
{
    result = false;
}

This effectively disables updating any profile information (although it can still be created). Now run the application again. Choose a person and try to edit this person’s name and profile information at the same time. If a profile hasn’t been created yet, the save will succeed, but if the profile was already created and is being updated, the name update will be saved but the application will return the error from the second save operation, like “The current user does not have permission to update entities in the EntitySet ‘PersonProfiles’”.

This is a known and valid behavior of the application as we designed it, but the error message is not particularly informative. We can offer something better by changing our save handler to return an extended error message:

myapp.onsavechanges = function (e) {
    var personData = myapp.activeDataWorkspace.DemoServiceData;
    e.detail.promise = personData.saveChanges()
        .then(function () {
            var profileData = myapp.activeDataWorkspace.ApplicationData;
            return profileData.saveChanges().then(null, function (errors) {
                errors.unshift({
                    message: "The person name was saved, but there was one " +
                        "or more errors saving the person profile information."
                });
                throw errors;
            });
        });
};

Now when you run the application and attempt the same update, you get a more informative error message:

image

Wrapping Up

This post illustrated an advanced feature of LightSwitch and Cloud Business Applications that handles saving data to multiple data sources when using the standard save gesture in the application UI. It showed how to handle the myapp.onsavechanges event to implement the necessary semantics for appropriately saving, and then how to provide an appropriate experience for the application user in cases where changes were not fully saved to all the data sources.

We hope this post helps to stimulate some interesting application scenarios, and as always, feel free to leave comments or questions on our forums. Until next time, happy coding!

Stephen Provine
Software Design Engineer
LightSwitch and Cloud Business Applications

Leave a Comment
  • Please add 2 and 8 and type the answer here:
  • Post
  • Nice article Stephen

  • This article contains a crucial piece of code that I had been searching for for quite some time. My use was for generating a "Case Number" or any Unique Identifier that auto-increments. It was quite difficult creating an auto-incrementing number based on the Id of the newly _created record. My goal was to end up with "2014-" & Entity.Id, so "2014-1, 2014-2, etc". Here is the code I modified from your article to achieve this in the HTML Client...

    myapp.AddEditTableName.created = function (screen) {

       // Get the current Date

       var d = new Date();

       if (typeof screen.TableName.Id !== "number") {

           myapp.activeDataWorkspace.ApplicationData.TableName.orderBy("Id").execute().then(function (tableName) {

               var lastRecord = tableName.results[tableName.results.length - 1];

               var nextRecord = lastRecord.Id + 1;

               screen.TableName.CaseNumber = d.getFullYear() + "-" + nextRecord;

           })

       };

    }

    And in case it helps anyone, here is the code I used to do the same with the Silverlight client...

    Private Sub CreateNewTableName_Created()

               ' Write your code here.

               Dispatchers.Current.BeginInvoke(Function()

                                                   Dim items As IEnumerable(Of TableName) = Me.DataWorkspace.ApplicationData.TableName.GetQuery().Execute()

                                                   If items.Count() > 0 Then

                                                       Me.TableNameProperty.CaseNumber = Year(Date.Today) & "-" & (items.LastOrDefault().Id + 1)

                                                   Else

                                                       Me.TableNameProperty.CaseNumber = 0

                                                   End If

                                               End Function)

           End Sub

  • Hi

    Help me please. I use form authification , and my "Person" table of this example - UserRegistration.

    But this table (User Registration) does not have ID filed (relationship by "username" filed), so how i can get last User Registration ID?

    Or how modify this code without ID?

    if (typeof screen.Person.ID !== "number") {

           myapp.activeDataWorkspace.DemoServiceData.Persons

               .orderBy("ID").execute().then(function (persons) {

                   var lastPerson = persons.results[persons.results.length - 1];

                   screen.Person.ID = lastPerson.ID + 1;

               });

       }

    Thanks.

  • Hi Ruslan,

    If I understand your scenario correctly, you are trying to create an instance of the built-in UserRegistration entity type at the same time as creating an additional entity like UserProfile in your intrinsic database. Indeed, the UserRegistration entity type does not have an integer-based key - the UserName property is the key, so you cannot implement any kind of auto-increment algorithm here. Instead, you will need to have your UI show the necessary properties from both the UserRegistration entity and the UserProfile entity and have the end-user fill out these properties.

    So in effect, you end up eliminating the code I used in the article, because there is no way to compute a new key value. Does this make sense?

Page 1 of 1 (4 items)