Oct
6
Written by:
Michael Washington
10/6/2013 9:34 PM
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
An administrator can create unlimited surveys.
Each survey can have unlimited multiple choice questions.
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.
A user is presented with surveys to take and they can view the survey results.
When a user takes a survey, the questions they can answer will be in Green.
Users can optionally add comments with their answer to each question.
Tabulated survey results can be viewed.
Comments for each question can also be viewed.
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
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.
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.
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
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"
});
};
});
};
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
});
};
});
};
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.
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...
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
|
@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
|
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
|
@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
|
@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
|
@Landon - Just blog on any LightSwitch thing that interests you!
By Michael Washington on
3/18/2014 11:30 AM
|
@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
|
@Landon - "Insert" is used to add records
By Michael Washington on
3/20/2014 1:26 PM
|
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
|
@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
|
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
|
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
|
@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
|
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
|
@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
|
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
|
@hishamash - It can be done but I don't have any examples, sorry.
By Michael Washington on
9/17/2014 10:14 PM
|