You are here:   Blog
Register   |  Login

 

Oct 6

Written by: Michael Washington
10/6/2013 9:34 PM  RssIcon

image

Note: You can use the live application at the following link: https://survey.lightswitchhelpwebsite.com/HTMLClient (use your username and password from LightSwitchHelpWebsite.com).

Handling complex business logic in a Single Page Application (SPA) is not a simple task. Process flow that is normally easily enforced in a traditional postback web application, can have security holes, because hackers can access the service methods directly. 

In this article we will implement a mobile enhanced survey application. It will allow an administrator the ability to create unlimited multiple choice surveys that can have unlimited questions. The complex business rule that will be enforced, will be one that requires a respondent to answer the questions in order. Even if a knowledgeable hacker tried to directly access the OData service to insert records out of order, the business rules will still be enforced and they will not be able to bypass them.

This is done using WCF RIA Services.

The sample application also uses the following code by Jewel Lambert:

 

The Application

 image

An administrator can create unlimited surveys.

image

Each survey can have unlimited multiple choice questions.

 

image

Users must log into to take surveys.

The application implements unique indexes to prevent a user from answering a question or survey more than once.

image

A user is presented with surveys to take and they can view the survey results.

 

image

When a user takes a survey, the questions they can answer will be in Green.

image

Users can optionally add comments with their answer to each question.

image

Tabulated survey results can be viewed.

image

Comments for each question can also be viewed.

image

If a user does not answer the preceding question, they will not be allowed to continue.

This is the business rule that is the subject of this blog post.

Overview of the Application

image

The diagram above shows the database tables the application uses.

However, we block off direct access to most of these tables using the following code:

 

namespace LightSwitchApplication
{
    public partial class ApplicationDataService
    {
        #region Surveys
        partial void Surveys_CanDelete(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        }
        partial void Surveys_CanInsert(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        }
        partial void Surveys_CanUpdate(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        } 
        #endregion
        #region SurveyQuestions
        partial void SurveyQuestions_Filter(ref Expression<Func<SurveyQuestion, bool>> filter)
        {
            // Apply filter if user does not have SecurityAdministration Permission
            if (!this.Application.User.HasPermission(Permissions.SecurityAdministration))
            {
                // This filter prevents any records from being read
                // The WCF RIA Service bypasses this filter and allows access
                filter = x => x.Id == -1;
            }
        }
        partial void SurveyQuestions_CanDelete(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        }
        partial void SurveyQuestions_CanInsert(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        }
        partial void SurveyQuestions_CanUpdate(ref bool result)
        {
            result = this.Application.User.HasPermission(Permissions.SecurityAdministration);
        } 
        #endregion
    }
}

 

This code blocks off access on the server-side, fully protecting the application from direct access  to its OData services.

image

The application contains a WCF RIA Service project that allows us to expose the data in a granular way.

The WCF RIA Service accesses the data from the LightSwitch database Entity Framework class directly, bypassing the OData security restrictions.

This allows us  to expose the data via our business rules. The WCF RIA Service methods we create are then exposed by LightSwitch as OData.

image

The primary reason you may want to use WCF RIA Services with Visual Studio LightSwitch is to:

  • Combine more than one entity into a single entity.
  • Eliminate unneeded columns in an entity to improve performance (otherwise large amounts of data, for example pictures, will be transmitted even when they are not shown).
  • Implement calculated fields that allow the resulting values to be searchable and sortable.

You can get more information on creating WCF RIA Services with LightSwitch at the following link:

http://lightswitchhelpwebsite.com/Blog/tabid/61/tagid/21/WCF-RIA-Service.aspx

Implementing the Business Rules

The first WCF RIA Service method we implement is GetAllQuestionsForUser():

 

        [Query(IsDefault = true)]
        public IQueryable<QuestionsForUser> GetAllQuestionsForUser()
        {
            // Get the current user
            string strCurrentUserName = System.Web.HttpContext.Current.User.Identity.Name;
            // We are under Forms Authentication so if user is blank then we
            // are debugging and we are TestUser
            if (strCurrentUserName == "")
            {
                strCurrentUserName = "TestUser";
            }
            // Get all the Questions For User
            var colQuestionsForUser = from Survey_Question in this.Context.SurveyQuestions
                                      orderby Survey_Question.Survey.Id
                                      orderby Survey_Question.Id
                                      // Shape the results into the 
                                      // QuestionsForUser class
                                      select new QuestionsForUser
                                      {
                                          QuestionId = Survey_Question.Id,
                                          UserName = strCurrentUserName,
                                          SurveyId = Survey_Question.SurveyQuestion_Survey,
                                          Question = Survey_Question.Question,
                                          isAnswered = ((Survey_Question.SurveyAnswers
                                          .Where(x => x.UserName == strCurrentUserName
                                              && x.SurveyQuestion.Id == Survey_Question.Id).Count()) > 0),
                                          isActive = false
                                      };
            // final collection
            List<QuestionsForUser> GetAllQuestionsForUserFinal = new List<QuestionsForUser>();
            // Flag for the current SurveyID
            int intCurrentSurveyID = -1;
            // Flag to indicate when at least one question is marked active
            bool boolNextQuestionLocated = false;
            // Loop through all questions
            foreach (var item in colQuestionsForUser)
            {
                // Possibly reset intCurrentSurveyID
                if (intCurrentSurveyID != item.SurveyId)
                {
                    // Reset intCurrentSurveyID
                    intCurrentSurveyID = item.SurveyId;
                    // Set boolNextQuestionLocated
                    boolNextQuestionLocated = false;
                }
                // If isAnswered set isActive to true
                item.isActive = (item.isAnswered);
                // Find the first non active question 
                if (!item.isActive && (boolNextQuestionLocated == false))
                {
                    item.isActive = true;
                    boolNextQuestionLocated = true;
                }
                GetAllQuestionsForUserFinal.Add(item);
            }
            return GetAllQuestionsForUserFinal.AsQueryable<QuestionsForUser>();
        }

 

This method uses a loop to determine if a question has been answered by the proceeding question.

We will consume the result of this method in the GetAllQuestionDetailForUser() method and use it as a constraint in the

UpdateQuestionDetailForUser(QuestionDetailForUser objQuestionDetailForUser) method (you can only insert a question that was returned by the GetAllQuestionsForUser() method).

The ability to consume the result of a WCF RIA Service method in another WCF RIA Service method allows you to implement a clean object oriented architecture with minimal code.

When a user clicks on a question to view it, so that they can answer it, the following WCF RIA Service method is called:

 

        [Query(IsDefault = true)]
        public IQueryable<QuestionDetailForUser> GetAllQuestionDetailForUser()
        {
            var colQuestionDetailsForUser = new List<QuestionDetailForUser>().AsQueryable();
            // Get the current user
            string strCurrentUserName = System.Web.HttpContext.Current.User.Identity.Name;
            // We are under Forms Authentication so if user is blank then we
            // are debugging and we are TestUser
            if (strCurrentUserName == "")
            {
                strCurrentUserName = "TestUser";
            }
            int? intNull = null;
            // Query the GetAllQuestionsForUser method because it calculates if a Question
            // is Active or not -- only return Question Details if it is Active
            var objUserQuestionIds = from QuestionForUser in this.GetAllQuestionsForUser()
                                     where QuestionForUser.isActive == true
                                     where QuestionForUser.UserName == strCurrentUserName
                                     select QuestionForUser.QuestionId;
            colQuestionDetailsForUser = (from Survey_Question in this.Context.SurveyQuestions
                                         where objUserQuestionIds.Contains(Survey_Question.Id)
                                         select new QuestionDetailForUser
                                         {
                                             QuestionId = Survey_Question.Id,
                                             SurveyId = Survey_Question.Survey.Id,
                                             UserName = strCurrentUserName,
                                             Question = Survey_Question.Question,
                                             Choice1 = Survey_Question.Choice1,
                                             Choice2 = Survey_Question.Choice2,
                                             Choice3 = Survey_Question.Choice3,
                                             Choice4 = Survey_Question.Choice4,
                                             SelectedChoice = ((from SurveyAnswers in Survey_Question.SurveyAnswers
                                                                where SurveyAnswers.SurveyQuestion.Id == Survey_Question.Id
                                                                where SurveyAnswers.UserName == strCurrentUserName
                                                                select SurveyAnswers).FirstOrDefault() != null)
                                                                ?
                                                                (from SurveyAnswers in Survey_Question.SurveyAnswers
                                                                 where SurveyAnswers.SurveyQuestion.Id == Survey_Question.Id
                                                                 where SurveyAnswers.UserName == strCurrentUserName
                                                                 select SurveyAnswers).FirstOrDefault().Choice
                                                                : intNull,
                                             Comments = ((from SurveyAnswers in Survey_Question.SurveyAnswers
                                                          where SurveyAnswers.SurveyQuestion.Id == Survey_Question.Id
                                                          where SurveyAnswers.UserName == strCurrentUserName
                                                          select SurveyAnswers).FirstOrDefault() != null)
                                                                ?
                                                                (from SurveyAnswers in Survey_Question.SurveyAnswers
                                                                 where SurveyAnswers.SurveyQuestion.Id == Survey_Question.Id
                                                                 where SurveyAnswers.UserName == strCurrentUserName
                                                                 select SurveyAnswers).FirstOrDefault().Comment
                                                                : ""
                                         });
            return colQuestionDetailsForUser.AsQueryable<QuestionDetailForUser>();
        }

 

You will notice that this method actually combines the SurveyQuestions and SurveyAnswers tables into one entity. For example, if the user has already answered this question it will display the original answer.

This allows us to optimize the data flow and makes it easier to consume and update the data on the client side.

 

The Client Side

image

When a user views the questions for a survey, only the questions they can answer will be in Green.

The following JavaScript code is used to call the WCF RIA Service to determine if it is active or not:

 

myapp.TakeSurvey.QuestionsForUsersTemplate_postRender = function (element, contentItem) {
    // Make Active questions Green
    // Get the QuestionId
    var QuestionId = contentItem.value.QuestionId;
    // Get the QuestionDetailForUsers record
    myapp.activeDataWorkspace.WCF_RIA_ServiceData
        .QuestionDetailForUsers_SingleOrDefault(QuestionId)
        .execute()
        .then(function (result) {
            // The QuestionDetail record
            var QuestionDetail = result.results[0];
            // If we have a QuestionDetail record at this point
            // then it is Active because the WCF RIA Service
            // only returns Active records
            if (QuestionDetail !== undefined) {
                $(element).parent().css({
                    "background-color": "green",
                    "background-image": "none",
                    color: "white"
                });
            };
        });
};

 

image

If a user clicks on a question that is not active they will see a popup message.

Otherwise they will be taken to the page to view and answer the question.

The following code is used:

 

myapp.TakeSurvey.QuestionsForUsers_ItemTap_execute = function (screen) {
    // Get selected QuestionId
    var QuestionId = screen.QuestionsForUsers.selectedItem.QuestionId;
    // Get the QuestionDetailForUsers record
    myapp.activeDataWorkspace.WCF_RIA_ServiceData
        .QuestionDetailForUsers_SingleOrDefault(QuestionId)
        .execute()
        .then(function (result) {
            // The QuestionDetail record
            var QuestionDetail = result.results[0];
            if (QuestionDetail !== undefined) {
                // Set the scrollTopPosition
                var scrollTopPosition = $(window).scrollTop();
                // Open the detail screen
                myapp.showAddEditQuestionDetailForUser(QuestionDetail, {
                    afterClosed: function (AddEditPageScreen, navigationAction) {
                        // After the Edit screen is closed 
                        // scroll to the saved scrollTopPosition
                        $(window).scrollTop(scrollTopPosition);
                        // Are there any changes ?
                        if (navigationAction === msls.NavigateBackAction.commit) {
                            // Save all changes on the screen
                            return myapp.applyChanges().then(function () {
                                // Reload the page to reflect any changes
                                screen.QuestionsForUsers.load();
                            });
                        }
                    }
                });
            } else {
                // Clicking on a non Active Question
                msls.showMessageBox("You must complete the previous question first.", {
                    title: "Cannot Proceed",
                    buttons: msls.MessageBoxButtons.ok
                });
            };
        });
};

 

image

When a user clicks on a question to view it, they are presented with a screen that allows them to view and answer the question.

image

The screen designer in LightSwitch shows that the screen is actually very simple because it contains only the entity returned by the GetAllQuestionDetailForUser() method.

Note that this page uses a JQuery Mobile radio button control by Jewel Lambert from her article: Using jQuery Mobile Radio Buttons in LightSwitch. This is an incredible control that was written to work with LightSwitch and does quite a bit more than what I used it for.

When a user saves the record the following WCF RIA Service is used:

 

        public void UpdateQuestionDetailForUser(QuestionDetailForUser objQuestionDetailForUser)
        {
            // Get the current user
            string strCurrentUserName = System.Web.HttpContext.Current.User.Identity.Name;
            // We are under Forms Authentication so if user is blank then we
            // are debugging and we are TestUser
            if (strCurrentUserName == "")
            {
                strCurrentUserName = "TestUser";
            }
            // Check for an existing Answer for this Question for this User
            var objSurveyAnswer = (from SurveyAnswers in this.Context.SurveyAnswers
                                   where SurveyAnswers.SurveyQuestion.Id == objQuestionDetailForUser.QuestionId
                                   where SurveyAnswers.UserName == strCurrentUserName
                                   select SurveyAnswers).FirstOrDefault();
            if (objSurveyAnswer != null)
            {
                try // This is an update ****
                {
                    // Set values
                    objSurveyAnswer.Choice = Convert.ToInt32(objQuestionDetailForUser.SelectedChoice);
                    objSurveyAnswer.Comment = objQuestionDetailForUser.Comments;
                    // Update LightSwitch Database
                    this.Context.SaveChanges(
                        System.Data.Objects.SaveOptions.DetectChangesBeforeSave);
                }
                catch (Exception ex)
                {
                    throw new Exception("Error inserting QuestionId " + objQuestionDetailForUser.QuestionId, ex);
                }
            }
            else // This is an Insert ****
            {
                // Query the GetAllQuestionsForUser method because it calculates if a Question
                // is Active or not for the QuestionId being inserted
                // If it is not in the collection, do not allow the insert
                var objUserQuestion = (from QuestionForUser in this.GetAllQuestionsForUser()
                                       where QuestionForUser.UserName == strCurrentUserName
                                       where QuestionForUser.QuestionId == objQuestionDetailForUser.QuestionId
                                       select QuestionForUser).FirstOrDefault();
                if (objUserQuestion != null)
                {
                    try
                    {
                        // Get the Survey Question
                        var objSurveyQuestion = (from SurveyQuestions in this.Context.SurveyQuestions
                                                 where SurveyQuestions.Id == objQuestionDetailForUser.QuestionId
                                                 select SurveyQuestions).FirstOrDefault();
                        // Create a SurveyAnswer object
                        SurveyAnswer objNewSurveyAnswer = this.Context.CreateObject<SurveyAnswer>();
                        // Set values
                        objNewSurveyAnswer.UserName = strCurrentUserName;
                        objNewSurveyAnswer.Choice = Convert.ToInt32(objQuestionDetailForUser.SelectedChoice);
                        objNewSurveyAnswer.Comment = objQuestionDetailForUser.Comments;
                        objNewSurveyAnswer.SurveyQuestion = objSurveyQuestion;
                        // Update LightSwitch Database
                        this.Context.SurveyAnswers.AddObject(objNewSurveyAnswer);
                        this.Context.SaveChanges(
                            System.Data.Objects.SaveOptions.DetectChangesBeforeSave);
                    }
                    catch (Exception ex)
                    {
                        throw new Exception("Error inserting QuestionId " + objQuestionDetailForUser.QuestionId, ex);
                    }
                }
                else
                {
                    throw new Exception("Error inserting Answer. Answer is not marked Active.");
                }
            }
        }

 

 

Links

Creating a WCF RIA Service for Visual Studio LightSwitch 2013

Creating a WCF RIA Service for Visual Studio 2012 (Update 2 and higher)

Jewel Lambert:

 

Download Code

The LightSwitch project is available at http://lightswitchhelpwebsite.com/Downloads.aspx

(you must have Visual Studio 2012 (or higher) installed to run the code)

17 comment(s) so far...


Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

Hello Michael,
I have been following your LightSwitch applications all the way through. I bought your book too. I like your application very much especially the tip and tricks. I am also very appreciate you have been shared all your work with us. Right now I am building a LightSwitch HTML Client application, i have some outstanding issues i could not figure it out myself. I just wonder could you please help me out? I could pay some money for your services.
Thanks,
--Wanda

By Wanda Zeng on   11/15/2013 2:52 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Wanda Zeng - Thank you for your support! I am sorry I am not available for contract support or work, However you can get help in the LightSwitch Forums at: http://social.msdn.microsoft.com/Forums/vstudio/en-US/home?forum=lightswitch

By Michael Washington on   11/15/2013 2:54 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

I am a bit confused about how the UpdateQuestionDetailForUser method is called when as user saves the data. How does lightswitch know to use this method? Is it automatic, or does it have to be manually set?

By Landon on   3/18/2014 10:15 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Landon - When you make a WCF RIA Service method that begins with the word "Update" and it takes as a parameter the Entity type, LightSwitch will know to call that method whenever the fields on the screen that are bound to that Entity type are updated.

By Michael Washington on   3/18/2014 10:17 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Michael - Thank you for the clarification. I have learned so much from this article, and the lightswitchhelpwebsite in general. Hopefully in some time I will be able to contribute back in some way.

By Landon on   3/18/2014 11:29 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Landon - Just blog on any LightSwitch thing that interests you!

By Michael Washington on   3/18/2014 11:30 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Michael - Like the "Update" method, is there a similar naming convention for Adding a brand new entry? I am trying to make a method to insert a new entry and my button shows up as read-only, and I have not been able to figure it out. I have got all my editing functionality done in my project, but I cannot add any new entries.

By Landon on   3/20/2014 1:25 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Landon - "Insert" is used to add records

By Michael Washington on   3/20/2014 1:26 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

I'm still baffled by RIA. Never done anything with WCF and running into limitations of having to return data in exact shape as entities. have worked around some of these with SQL views due to time limitations.

I have a few dumb questions. I read many of the links about lightswitch and RIA, but don't understand much of the terminology (e.g. Domain services), so I'm lost almost straight away. I'm willing to buy a book or take a course on plural sight or wherever, but want to make sure i choose the right one as there are very few books specific to LS and I don't want to waste time learning the wrong subject.

I have a desktop silverlight app that's deployed via a UNC path. What's required to use RIA? I'm assuming it's going to involve having to use IIS and creating a WCF service(?) on the IIS server to consume the http calls.

Let's assume I want to use just one RIA call for joined tables (no FK link), do I have to move to deploying via IIS.

Can someone point me in right direction?

Thanks

Mark

By Mark Anderson on   4/11/2014 3:03 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Mark Anderson - You don't want to study all of WCF RIA Services because LightSwitch just uses a simple specific implementation. Click on the WCF RIA Services link under Topics on this page and tread those blogs. That should cover all you need.

By Michael Washington on   4/11/2014 3:18 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

Thanks Michael

got through the tutorial today. Very good

Will have ago at replacing my 3 SQL views with RIA next week

Regards

mark

By Mark Anderson on   4/12/2014 6:26 PM
Gravatar

2013 Book?

Hi Michael

I'd like to buy your book, but am reluctant to buy a 2012 version. I bought a 2012 Lightswitch book and wasted a lot of time translating stuff to 2013. Do you have a 2013 version planned?

Regards

mark

By Mark Anderson on   4/13/2014 5:22 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Mark Anderson - Everything still applies. Some of the screens changed. However, I am working on a 2013 version of the book that should be complete in a few months.

By Michael Washington on   4/13/2014 7:51 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

Hi Michael - You have great stuff and TIPs in your web site.
I installed the lsSurbey but igot this warning.(enclosed) might be due to my LS 2013? I couldn't update the code to 2013 tools. pls assist.
Regards and all the best,
Yuval
*********************
Warning 1 The type 'LightSwitchApplication.Implementation.ApplicationData' in
'C:\Lightswitch after ZIP survey\lsSurvey\lsSurvey.Server\GeneratedArtifacts\ApplicationDataObjectContext.cs'
conflicts with the imported type 'LightSwitchApplication.Implementation.ApplicationData' in
'c:\Lightswitch after ZIP survey\WCF_RIA_Project\bin\Release\WCF_RIA_Project.dll'.
Using the type defined in 'C:\Lightswitch after ZIP survey\lsSurvey\lsSurvey.Server\GeneratedArtifacts\ApplicationDataObjectContext.cs'.
C:\Lightswitch after ZIP survey\lsSurvey\lsSurvey.Server\GeneratedArtifacts\DataServiceImplementation.cs
17 130 lsSurvey.Server

By Yuval Granit on   5/20/2014 3:45 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@Yuval Granit - I am sorry but upgrading a project that uses WCF RIA services is very tricky (but it is possible). I am sorry but due to my other work I am unable to offer personal assistance. Please post specific errors encountered during the upgrade to the official LightSwitch forums at: http://social.msdn.microsoft.com/Forums/vstudio/en-US/home?forum=lightswitch

By Michael Washington on   5/20/2014 3:53 AM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

Hi Michael ,

Its an excellent tutorial. Appreciate all the help and code you have provided us.

I had a few queries regarding the survey app.

1) How can we restrict user to submit a survey only once. Once the user submits a survey he shouldnt be able to modify it.

Thanks

Hisham

By hishamash on   9/17/2014 9:48 PM
Gravatar

Re: LightSwitch Survey: Handling Complex Business Logic Using WCF RIA Services

@hishamash - It can be done but I don't have any examples, sorry.

By Michael Washington on   9/17/2014 10:14 PM

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation