none
Programmatically add new data item in HTML Client as part of a entity set relationship using parameters RRS feed

  • Question

  • In my application, a Patient entity has a 1-to-many relationship with HospitalData, so when I want to add a new hospitalization for a patient I can sometimes use the existing showAddNewHospitalization method with (New HospitalData) as the parameter for HospitalData in the screen creation wizard.

    But I need to do that programmatically now.  In myapp.AddNewHospitalization the 2 required arguments are "array parameters" and "dataWorkspace" (default is new).  See below.

    What is the first parameter "array"?  The array index of the current record (i.e., screen.Patient.Id)?

    What are the options and the specific syntax for the second parameter?  If default, is it "new" or something additional?

    Tuesday, May 7, 2013 5:11 PM

Answers

  • Thanks for your suggestion, Huy. I had seen your sample project you refer to and have it in mind for another part of my application, but for this part it would have required extensive re-working of queries and screen navigations that I had already made and hoped not to re-do.  In hindsinght, it may have been easier your way! But, my screen navigation flow goes:

    Patient -->  Hospitalization --> Physician Charges

    In any case, after looking at the "complex" dynamic add scenario in Michael's blog post, I have been able to figure it out.  I was scratching my head for a while at the "results.results[0]" part in the code looking for a first product.  But from what I have been able to figure out by stepping through code to add a new entity is that whenever LS adds new elements to the Visual Collection it adds them to the beginning (.unshift()), so the newest item has an array index of zero.

    One upshot of this is that in the blog example the conditional to check if FirstProduct has value is superfluous and can safely be omitted.

    After successfully connecting an existing patient's record with a new hospitalization, I still would end up with a duplicate patient record that was a fragment (i.e., not connected to any other data).  To prevent that from happening I had to delete the [0]-index entity (the new record) in the beforeApplyChanges for patients that already existed in the database.

    myapp.AddNewAdmitOrConsult.AddHospitalData_Tap_execute = function (screen) {
        // Write code here.
        var allPatients = screen.Patients.data;
        var firstName = screen.Patient.FirstName;
        var lastName = screen.Patient.LastName;
        
        var fullNameArray = [];
        // Check to see if a first and last name have been entered.
        if (firstName === null || firstName === undefined || lastName === null || lastName === undefined) {
                msls.showMessageBox("Both first and last names must be entered.", {
                title: "Patient Name",
                buttons: msls.MessageBoxButtons.ok}).then(function (result) {
                if (result===msls.MessageBoxResult.ok) {
                    return;
                };
                });
        }
        else {
            var thisFullName = firstName + " " + lastName;
            for (var index in allPatients) {
                fullNameArray[index] = allPatients[index].details._.FirstName + " " + allPatients[index].details._.LastName;
            }  // create array of all existing patients First Name + Last Name then compare to current
            var ckDuplicate = fullNameArray.lastIndexOf(thisFullName);
            if (ckDuplicate == 0)  // no match is found; new patient
            {
                var newHospitalization = new myapp.HospitalData();
                newHospitalization.setPatient(screen.Patient);
                myapp.showAddNewHospitalization(newHospitalization);
                return;
            }
            else {
                screen.DuplicateFound = true;
                var newHospitalization = new myapp.HospitalData();
                var archivedPatient =  allPatients[ckDuplicate]; 
                newHospitalization.setPatient(archivedPatient);  // the array index of the archived patient is now linked
                myapp.showAddNewHospitalization(newHospitalization);
            }
            }
            
    };
    
    myapp.AddNewAdmitOrConsult.beforeApplyChanges = function (screen) {
        // Write code here.
        if (screen.DuplicateFound) {
            screen.Patients.data[0].deleteEntity();   // stops the program from saving the patient twice
            screen.DuplicateFound = false;
        }
    };

    Saturday, May 11, 2013 6:55 AM

All replies

  • In hopes of getting my overall problem solved in addition to the question above answered, I will explain a little bit more what I'm trying to do.

    In the HTML client, property validation does not occur until the server is accessed, typically when the "save" button is pressed.  This is in contrast to the SL client, which had property validation that could be invoked immediately after a field was entered by the user.  It will be annoying to any user that fills out multiple fields in a form to then press save and find out that some fields are invalid per the server.  Client-side property validations, it is my understanding, have to individually coded to make up for this.

    I have written a button tap method that takes the first and last names of the patient that the user has entered in a new patient form and checks to see if any patients' names already on the server match the current name.  If a match is found, the Id of the existing patient is extracted.  The button tap would normally take the user to the next dialog where new hospitalization data is entered (see post above).  If a match is found in the database for an existing patient, I want to create the new hospitalization dialog programmatically using the existing patient Id.  If no existing patient is found, a new hospitalization data dialog is created along with the new patient record.

    myapp.AddNewAdmitOrConsult.AddHospitalData_Tap_execute = function (screen) {
        // Write code here.
        var allPatients = screen.Patients.data;
        var firstName = screen.Patient.FirstName;
        var lastName = screen.Patient.LastName;
        
        var fullNameArray = [];
        if (firstName === null || firstName === undefined || lastName === null || lastName === undefined) {
                msls.showMessageBox("Both first and last names must be entered.", {
                title: "Patient Name",
                buttons: msls.MessageBoxButtons.ok}).then(function (result) {
                if (result===msls.MessageBoxResult.ok) {
                    return;
                };
                });
        }
        else {
            var thisFullName = firstName + " " + lastName;
            for (var index in allPatients) {
                fullNameArray[index] = allPatients[index].details._.FirstName + " " + allPatients[index].details._.LastName;
            }  // create array of all existing patients First Name + Last Name then compare to current
            var ckDuplicate = fullNameArray.lastIndexOf(thisFullName);
            var patientId = allPatients[ckDuplicate].details._.Id;  // get the Id of the existing patient
            if (patientId == undefined)
                {
                patientId=null;
                }
            var newHospitalization = new myapp.AddNewHospitalization(patientId, msls.application.activeDataWorkspace.ApplicationData.HospitalData);
            myapp.showAddNewHospitalization(newHospitalization);
        }
    };

    The problem I am having is I do not know how to call the addNewHospitalization() and showAddNewHospitalization() methods with the correct parameters to achieve this.  My code for checking and finding duplicate patients and returning an Id if found or null if none is working correctly.

    Not sure if it helps, but when I click on the button to invoke the show, the error from the myapp.showAddNewHospitalization() is "e.forEach is not a function."

    Any assistance kindly appreciated.

    Thursday, May 9, 2013 5:20 PM
  • This should help:

    Dynamically Creating Records In The LightSwitch HTML Client

    You will see that I am able to create any records I need (including setting properties for related tables).


    The Visual Studio LightSwitch Marketplace

    http://LightSwitchHelpWebsite.com

    Thursday, May 9, 2013 5:55 PM
  • Thanks, Michael.  That gets me a little closer to what I'm trying to do.  BTW, I am an avid follower of your blog and had seen this post and was examining if the "complex" dynamic add you use could apply.  Your code though dynamically adds new records as I understand it and I need to add a new record if one doesn't exist and update an old one if it does.

    Revising my code using your blog, it becomes:

    .........
    
            var patientId = allPatients[ckDuplicate].details._.Id;  // get the Id of the existing patient
            if (patientId == undefined)  // no matches
            {
                var newHospitalization = new myapp.HospitalData();
            }
            else {
                var newHospitalization = myapp.HospitalData(?);
            }  // argument needed here to connect to existing patient entity I think
            newHospitalization.setPatient(screen.Patient);
            myapp.showAddNewHospitalization(newHospitalization);
        }
    };

    When I examine my oData for the associated entities between Patient and Hospital, I find as example this patient with ID # 144:

    <link  rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/HospitalDatas" type="application/atom+xml;type=feed" title="HospitalDatas" href="Patients(144)/HospitalDatas" /> 

    in the Patient table and

    <link  rel="http://schemas.microsoft.com/ado/2007/08/dataservices/related/Patient" type="application/atom+xml;type=entry" title="Patient" href="HospitalData(144)/Patient" /> 
    .
    .
    .
    <d:HospitalData_Patient m:type="Edm.Int32">144</d:HospitalData_Patient> 

    Without somehow passing this existing Id number from Patient() to HospitalData(), I'm not sure how to link them and not create a duplicate record.

    Thursday, May 9, 2013 7:22 PM
  • In my example at:

    Dynamically Creating Records In The LightSwitch HTML Client

    I cover two situations that I think should help you. In one example I detect what Order they are on and pass it as a paramater ( .setOrder(screen.Order) ):

    // Make a new OrderDetail
        var newOrderDetail = new myapp.OrderDetail();
        // Set the Order property
        // Whenever you have associated Entities, there will 
        // be a .set[Entity Name] method available
        newOrderDetail.setOrder(screen.Order);

    In another one, I need to search the database to find the record I need to add:

     // Try to find a Product
        var Products = screen.details.dataWorkspace.ApplicationData.Products
            .load().then(function (results) {
                // Try to get the first Product
                var FirstProduct = results.results[0];
                // Did we find a first Product?
                if (FirstProduct != undefined && FirstProduct != null) {
                    // Set the first Product as the Product for the OrderDetail
                    newOrderDetail.setProduct(FirstProduct);
                }
            });

    I have an article were I describe how to open screens and set values on the screens:

    Visual Studio LightSwitch Screen Navigation and Advanced JavaScript Examples

    That is all I have so far. I am learning this as I go like everyone else :)


    The Visual Studio LightSwitch Marketplace

    http://LightSwitchHelpWebsite.com


    Thursday, May 9, 2013 9:20 PM
  • Hi Allen,

    It looks like this post about using Screen instead of Modal Picker fits pretty nice with your scenario. The flow would be:

    1. Screen 1: Add new Hospitalization Data.
    2. Click Select Patient. Screen 2 pops up that searches for Patient by First / Last Name.
    3. If there's none match, user can click on Add New Patient on Screen 3 and add a new Patient. Once user is done on Screen 3, the app navigate back to Screen 1.

    I have a sample app with a simplified scenario that you can try out.

    If you prefer a different flow, can you describe it in terms of Entities and Screen Navigations? I'll try the best I can to help.

    Best regards,
    Huy


    Friday, May 10, 2013 5:54 PM
  • Thanks for your suggestion, Huy. I had seen your sample project you refer to and have it in mind for another part of my application, but for this part it would have required extensive re-working of queries and screen navigations that I had already made and hoped not to re-do.  In hindsinght, it may have been easier your way! But, my screen navigation flow goes:

    Patient -->  Hospitalization --> Physician Charges

    In any case, after looking at the "complex" dynamic add scenario in Michael's blog post, I have been able to figure it out.  I was scratching my head for a while at the "results.results[0]" part in the code looking for a first product.  But from what I have been able to figure out by stepping through code to add a new entity is that whenever LS adds new elements to the Visual Collection it adds them to the beginning (.unshift()), so the newest item has an array index of zero.

    One upshot of this is that in the blog example the conditional to check if FirstProduct has value is superfluous and can safely be omitted.

    After successfully connecting an existing patient's record with a new hospitalization, I still would end up with a duplicate patient record that was a fragment (i.e., not connected to any other data).  To prevent that from happening I had to delete the [0]-index entity (the new record) in the beforeApplyChanges for patients that already existed in the database.

    myapp.AddNewAdmitOrConsult.AddHospitalData_Tap_execute = function (screen) {
        // Write code here.
        var allPatients = screen.Patients.data;
        var firstName = screen.Patient.FirstName;
        var lastName = screen.Patient.LastName;
        
        var fullNameArray = [];
        // Check to see if a first and last name have been entered.
        if (firstName === null || firstName === undefined || lastName === null || lastName === undefined) {
                msls.showMessageBox("Both first and last names must be entered.", {
                title: "Patient Name",
                buttons: msls.MessageBoxButtons.ok}).then(function (result) {
                if (result===msls.MessageBoxResult.ok) {
                    return;
                };
                });
        }
        else {
            var thisFullName = firstName + " " + lastName;
            for (var index in allPatients) {
                fullNameArray[index] = allPatients[index].details._.FirstName + " " + allPatients[index].details._.LastName;
            }  // create array of all existing patients First Name + Last Name then compare to current
            var ckDuplicate = fullNameArray.lastIndexOf(thisFullName);
            if (ckDuplicate == 0)  // no match is found; new patient
            {
                var newHospitalization = new myapp.HospitalData();
                newHospitalization.setPatient(screen.Patient);
                myapp.showAddNewHospitalization(newHospitalization);
                return;
            }
            else {
                screen.DuplicateFound = true;
                var newHospitalization = new myapp.HospitalData();
                var archivedPatient =  allPatients[ckDuplicate]; 
                newHospitalization.setPatient(archivedPatient);  // the array index of the archived patient is now linked
                myapp.showAddNewHospitalization(newHospitalization);
            }
            }
            
    };
    
    myapp.AddNewAdmitOrConsult.beforeApplyChanges = function (screen) {
        // Write code here.
        if (screen.DuplicateFound) {
            screen.Patients.data[0].deleteEntity();   // stops the program from saving the patient twice
            screen.DuplicateFound = false;
        }
    };

    Saturday, May 11, 2013 6:55 AM
  • Hi Allen,

    Some of my observations / suggestions:

    Getting patient's FirstName and LastName

            for (var index in allPatients) {
                fullNameArray[index] = allPatients[index].details._.FirstName + " " + allPatients[index].details._.LastName;
            }

    You should not use details._., you can simply use allPatients[index].FirstName and allPatients[index].LastName.

    Checking for an existing Patient

    It looks to me like you're loading all Patients to the client and try to search for a patient on the client. This may work fine in a test application, but once your patients grow to 10,000, your application will loading a very large amount of data. Instead, you can use the query capability of LIGHTSWITCH to search for an existing client. For example

        var filter = "(FirstName eq " + msls._toODataString(firstName, ":String") +
                ") and (LastName eq " + msls._toODataString(lastName, ":String") + ")";
        myapp.activeDataWorkspace.ApplicationData
            .Patients
            .filter(filter)
            .execute()
            .then(function (result) {
                var customer = result.results[0];
    
            }, function (error) {
            });

    Doing this will introduce async complexity, but it will really save bandwidth for your HTML Client once your data grows.

    Showing the AddNewHospitalization screen

    I'm pretty sure you can find out the sample code in Michael's blog and other posts I made. You should not create the new Hospitalization, then launch the new screen. Because if the user cancel the new screen, the new Hospitalization is not discarded and you end up with a 'limbo' Hospitalization. You want to launch the screen with a null Hospitalization, then using beforeShow function to create the new Hospitalization there.

    Searching for the Patient

    I'm really not sure that a patient uniqueness can be determined by just first name and last name. Once you start getting more and more requirements (such as filter by date of birth / address), I think the current approach will become very complex. That's why I strongly suggest going for New Hospitalization -> Search or Create Patient route.

    Best regards,
    Huy



    Sunday, May 12, 2013 4:28 AM
  • Checking for an existing Patient

    It looks to me like you're loading all Patients to the client and try to search for a patient on the client. This may work fine in a test application, but once your patients grow to 10,000, your application will loading a very large amount of data. Instead, you can use the query capability of LIGHTSWITCH to search for an existing client. For example

        var filter = "(FirstName eq " + msls._toODataString(firstName) +
                ") and (LastName eq " + msls._toODataString(lastName) + ")";
        myapp.activeDataWorkspace.ApplicationData
            .Patients
            .filter(filter)
            .execute()
            .then(function (result) {
                var customer = result.results[0];
    
            }, function (error) {
            });

    Doing this will introduce async complexity, but it will really save bandwidth for your HTML Client once your data grows.


    That is really great powerful stuff!

    Also, that is the best example so far of doing a client side JavaScript query (I was unable to figure out the filter syntax from previous examples so I had to pass on blogging on it).

    Now, I will do a blog on that soon :)

    However, I tend to stay away from the methods that begin with "__" so what does "msls._toODataString" do? I would rather find some JavaScript library that will do the same thing and call it directly. I would hate for my code to break in a future release.


    The Visual Studio LightSwitch Marketplace

    http://LightSwitchHelpWebsite.com

    Sunday, May 12, 2013 12:50 PM
  • Huy, thank you for your coding suggestions and corrections.  I had resorted to the details._. syntax not by choice but by using the Locals and Watch windows to determine the shapes containing my data, and the preferred format you suggest using was not obvious this way.  This is, unfortunately, another shortcoming of Intellisense and Javascript that we have already discussed.  To overcome this problem, considerable grassroot effort along with several of the commercial 3rd-party control development companies (e.g., Telerik, Infragistics, Component One) are leveraging TypeScript (see here and also here) for its ability to provide robust declaration files (.d.ts) that IntelliSense can consistently use.  Providing this or something like it, along with the detailed MSDN encyclopedia of the LightSwitch API (esp. the HTML client) would be what I would wish the LS development team would prioritize.

    Regarding your suggested method of querying LS using oData, I had not seen this used before, and only one other forum post provides some information about querying LS data that I could find.  To me, this raises an interesting contrast between the advantages/disadvantages of using the LS DataServiceQuery API versus the ServerApplicationContext WebAPI approach with regard to server communication efficiency.  I may be mistaken, but your suggested query method still requires an implicit .load() operation which retrieves as in this case all the Patient data (which I can see under "myapp.activeDataWorkspace.ApplicationData.Patients._loadedEntities").  Perhaps only the ServerApplicationContext with its full LINQ capabilities is the real winner when it comes to power and efficiency for server requests?

    I appreciated the information you provided for the .filter() method using the _msls.toODataString() API; however, I should mention for anyone else using it that the _toOdataString() method requires two arguments:


    In this case, the correct syntax is slightly different:

    var filter = "(FirstName eq " + msls._toODataString(firstName, ":String") +
                ") and (LastName eq " + msls._toODataString(lastName, ":String") + ")";

    Anyone interested in seeing the other datatypes can refer to msls-1.0.0 lines 6428-512.

    Lastly, I agree with your suggestions regarding potential problems with using a patient's first and last name solely as a unique identifier. I will have to experiment with the alternate approach you suggest. However, this HTML Client app has a specific purpose which is why I have kept it very simple in this regard:  to allow physicians to enter in basic information about patients admitted while on overnight call, which out of necessity needs to be very quick and efficient. In general, I have left it to the Silverlight client to manage the database for duplicates, etc.



    Monday, May 13, 2013 3:32 AM
  • Hi,

    Client Query on HTML Client

    I completely agree that a full documentation is the goal we should aim to achieve, but we have to work with our current resources. Here's a list of query operations supported by HTML Client (from msls vsdoc.js).

    filter: function filter(expression) {
        /// <summary>
        /// Filters results using an expression defined
        /// by the OData $filter system query option.
        /// </summary>
        /// <param name="expression" type="String">
        /// An OData filter expression.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    orderBy: function orderBy(propertyName) {
        /// <summary>
        /// Orders results by a property in ascending order.
        /// </summary>
        /// <param name="propertyName" type="String">
        /// A property name.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    orderByDescending: function orderByDescending(propertyName) {
        /// <summary>
        /// Orders results by a property in descending order.
        /// </summary>
        /// <param name="propertyName" type="String">
        /// A property name.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    thenBy: function thenBy(propertyName) {
        /// <summary>
        /// Further orders results by a property in ascending order.
        /// </summary>
        /// <param name="propertyName" type="String">
        /// A property name.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    thenByDescending: function thenByDescending(propertyName) {
        /// <summary>
        /// Further orders results by a property in descending order.
        /// </summary>
        /// <param name="propertyName" type="String">
        /// A property name.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    expand: function expand(expression) {
        /// <summary>
        /// Expands results by including additional navigation properties using
        /// an expression defined by the OData $expand system query option.
        /// </summary>
        /// <param name="expression" type="String">
        /// An OData expand expression (a comma-separated
        /// list of names of navigation properties).
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    skip: function skip(count) {
        /// <summary>
        /// Bypasses a specified number of results.
        /// </summary>
        /// <param name="count" type="Number">
        /// The number of results to skip.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    top: function top(count) {
        /// <summary>
        /// Restricts results by a specified number.
        /// </summary>
        /// <param name="count" type="Number">
        /// The number of results to return.
        /// </param>
        /// <returns type="msls.DataServiceQuery" />
    },
    
    includeTotalCount: function includeTotalCount() {
        /// <summary>
        /// Requests that the total result count as if the skip and top
        /// operators were not applied is returned in addition to the results.
        /// </summary>
        /// <returns type="msls.DataServiceQuery" />
    }
    

    Filter query

    We lack the nice support of LINQ to build up the filter, so on HTML Client we only support the filter string following OData specification here. But here's how I often cheat :) I will build up a Screen Query using Screen and Query Designer. Then I will open Client\GeneratedArtifacts\viewModel.js to see what the tool generates for me :)

    msls._toODataString

    Allen, thank you very much for catching my coding mistake. I'll update my code sample.

    Michael: Unlike the properties / methods that start with _ on objects, _toODataString starts with _ because it was meant to be hidden from Intellisense. It has to be there to support all Screen Queries in generated viewModel.js, so there will be no major changes to it that I can see. I guess the original thinking is that you will build up your filter query according to spec and using constants. But it turns out it's very useful to build up query filter using variables as well.

    Best regards,
    Huy

    Monday, May 13, 2013 6:08 PM
  • Huy,

    This additional information you have provided is priceless. Thank You.

    I will visit all this in a comprehensive Blog post in the next two weeks.


    The Visual Studio LightSwitch Marketplace

    http://LightSwitchHelpWebsite.com

    Monday, May 13, 2013 6:32 PM
  • Searching for the Patient

    I'm really not sure that a patient uniqueness can be determined by just first name and last name. Once you start getting more and more requirements (such as filter by date of birth / address), I think the current approach will become very complex. That's why I strongly suggest going for New Hospitalization -> Search or Create Patient route.

    Best regards,
    Huy



    I created an example here:

    HUY Volume II - Visual Studio LightSwitch Advanced JavaScript Examples
    http://lightswitchhelpwebsite.com/Blog/tabid/61/EntryId/1203/HUY-Volume-II-Visual-Studio-LightSwitch-Advanced-JavaScript-Examples.aspx


    The Visual Studio LightSwitch Marketplace

    http://LightSwitchHelpWebsite.com

    Monday, May 27, 2013 7:43 PM
  • Huy,

    Your answers are a priceless resource to folks who are trying to do things with HTML Client that are not yet documented.  Thanks for that!

    Can you give an example of refreshing a screen collection with the results of a filter query? (pure js rather than preprocessquery method)

    Like this question:

    HTML Client: Javascript Apply Filter to Screen Data Collection

    Thanks again,

    Josh


    Tuesday, October 1, 2013 3:48 PM