// it's not worth changing this now public void ProcessPageLoad( PackageStore packageStore, GetSessionTitle getSessionTitle, TryGetViewInfo tryGetViewInfo, TryGetAttemptInfo tryGetAttemptInfo, AppendContentFrameDetails appendContentFrameDetails, RegisterError registerError, GetErrorInfo getErrorInfo, ProcessSessionEnd processSessionEnd, ProcessViewRequest processViewRequest, GetFramesetMsg getFramesetMsg, // messages that appear that are unique to different framesets bool isPostBack) { try { this.RegisterError = registerError; this.GetErrorInfo = getErrorInfo; this.AppendContentFrameDetails = appendContentFrameDetails; this.GetFramesetMsg = getFramesetMsg; this.mGetSessionTitle = getSessionTitle; this.mIsPostedPage = isPostBack; AttemptItemIdentifier attemptId; SessionView view; this.LoadContentFrame = true; this.ActivityHasChanged = false; if (!tryGetViewInfo(false, out view)) { this.WriteError( ResHelper.GetMessage(Localization.GetMessage("FRM_ViewNotSupportedMsg"))); return; } switch (view) { case SessionView.Execute: { if (!tryGetAttemptInfo(false, out attemptId)) { return; } this.Session = new StoredLearningSession(view, attemptId, packageStore); if (!processViewRequest(view, this.Session)) { return; } // If the session has ended, allow the application to deal with it. processSessionEnd(this.Session, ref this.mSessionEndedMsgTitle, ref this.mSessionEndedMsg); } break; case SessionView.Review: { if (!tryGetAttemptInfo(false, out attemptId)) { return; } this.Session = new StoredLearningSession(view, attemptId, packageStore); // Do not set logging options in review view. if (!processViewRequest(view, this.Session)) { return; } } break; case SessionView.RandomAccess: { // Note: RandomAccess is not supported in BWP, however that would have been caught before // displaying this frame. if (!tryGetAttemptInfo(false, out attemptId)) { return; } this.Session = new StoredLearningSession(view, attemptId, packageStore); // Do not set logging options in random access view. if (!processViewRequest(view, this.Session)) { return; } // Move to the first activity with a resource. MoveToNextActivity(this.Session); } break; default: this.WriteError( ResHelper.GetMessage( Localization.GetMessage("FRM_ViewNotSupportedMsg"))); return; } // If the page is posted, process posted data. Remember that all posted data should be considered hostile! // Note that if the session has already ended, then none of the posted data is saved or processed. if (isPostBack && !this.SessionIsEnded) { // Process any datamodel changes before doing any navigation. This does not save any data. this.ProcessDataModelValues( this.Request.Form[HiddenFieldNames.DataModel], this.Request.Form[HiddenFieldNames.ObjectiveIdMap]); // Assume we do not have to reload the content frame this.ActivityHasChanged = false; this.LoadContentFrame = false; // if the view requires more data, get it if ((this.Session.View == SessionView.Review) || (this.Session.View == SessionView.RandomAccess)) { // Get the current activity from the posted data and set the session to that activity string strActivityId = this.Request.Form[HiddenFieldNames.ActivityId]; long activityId; if (string.IsNullOrEmpty(strActivityId) || !long.TryParse(strActivityId, out activityId)) { this.WriteError( ResHelper.GetMessage( Localization.GetMessage("HID_InvalidActivityId"), strActivityId)); } else { MoveToActivity(this.Session, activityId); } } // Find out what the commands are and do them. this.mSaveOnly = true; ICollection<CommandInfo> commands = this.GetCommands(); foreach (CommandInfo cmdInfo in commands) { switch (cmdInfo.Command) { case Commands.DoNext: { if (!this.Session.HasCurrentActivity || !ProcessNavigationRequests(this.Session)) { if (this.Session.IsMoveToNextValid()) { MoveToNextActivity(this.Session); this.ActivityHasChanged = true; this.LoadContentFrame = true; } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; continue; } } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; } if (!this.ActivityHasChanged) { // Moving to the next activity is not valid. It's possible that when the current // activity was unloaded, it exited or suspended itself. If that's the case, we // try to reactivate it. Note this causes the attempt count on the activity to // increment, so on "poorly" written content, the user may not see their data // anymore. this.ActivateCurrentActivity(); this.WriteError( ResHelper.Format( this.GetFramesetMsg(FramesetStringId.MoveToNextFailedHtml), this.ThemeFolderPath), true); } this.mSaveOnly = false; } break; case Commands.DoPrevious: { if (!this.Session.HasCurrentActivity || !ProcessNavigationRequests(this.Session)) { if (this.Session.IsMoveToPreviousValid()) { MoveToPreviousActivity(this.Session); this.ActivityHasChanged = true; this.LoadContentFrame = true; } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; continue; } } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; } if (!this.ActivityHasChanged) { // Moving to the previous activity is not valid. It's possible that when the current // activity was unloaded, it exited or suspended itself. If that's the case, we // try to reactivate it. Note this causes the attempt count on the activity to // increment, so on "poorly" written content, the user may not see their data // anymore. this.ActivateCurrentActivity(); this.WriteError( ResHelper.Format( this.GetFramesetMsg(FramesetStringId.MoveToPreviousFailedHtml), this.ThemeFolderPath), true); } this.mSaveOnly = false; } break; case Commands.DoChoice: { // This command is used to navigate to activities, primarily from either within a link in an // error page, or the submit page returning to the current activity. This command does not // create a new attempt if the requested activity is already the current activity. // Requesting to move to a specific activity. The cmdData will include a numeric activity id. string cmdData = cmdInfo.CommandData; long activityId; if (long.TryParse(cmdData, out activityId)) { // If the requested activity is the current activity, then do not do the navigation. // We skip it because moving to the activity basically exits the current attempt and creates // a new one and in this case, that is not the desired behavior. // That new one also increments the attempt count. If we don't do the move, we // pretend it was done. This will force the content frame to be reloaded with the current // activity. if (this.IsCurrentActiveActivity(activityId)) { this.ActivityHasChanged = true; this.LoadContentFrame = true; } else { // If there is no current activity, or if any navigation requests did not // result in a move, then continue with the choice. if (!this.Session.HasCurrentActivity || !ProcessNavigationRequests(this.Session)) { if (this.Session.IsMoveToActivityValid(activityId)) { MoveToActivity(this.Session, activityId); this.ActivityHasChanged = true; this.LoadContentFrame = true; } } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; } } } if (!this.ActivityHasChanged) { // Moving to the selected activity is not valid. It's possible that when the current // activity was unloaded, it exited or suspended itself. If that's the case, we // try to reactivate it. Note this causes the attempt count on the activity to // increment, so on "poorly" written content, the user may not see their data // anymore. this.ActivateCurrentActivity(); this.WriteError( ResHelper.Format( this.GetFramesetMsg(FramesetStringId.MoveToActivityFailedHtml), this.ThemeFolderPath), true); } this.mSaveOnly = false; } break; case Commands.DoTocChoice: { // This command is used to navigate to activities in response to a user selecting a node // in the TOC. In this case, even if the current activity is the activity that is requested, // then a MoveToActivity() is requested. This will cause the attempt count on the activity // to be incremented and the RTE to be reinitialized. This may be a surprise to the user, but // is a requirement of the SCORM 2004 conformance tests. // If the selected page is the submit page (either in Execute or RandomAccess views), then // display the message and don't ask the session to move to a new activity. string cmdData = cmdInfo.CommandData; if (string.CompareOrdinal(cmdData, SubmitId) == 0) { // Requesting submit page. Do not change the current activity, but mark it as changed so that // it appears to the user that it has changed. this.ActivityHasChanged = true; this.LoadContentFrame = true; string title = this.GetFramesetMsg(FramesetStringId.SubmitPageTitleHtml); string message; string saveBtn = this.GetFramesetMsg(FramesetStringId.SubmitPageSaveButtonHtml); if (this.Session.HasCurrentActivity) { message = this.GetFramesetMsg(FramesetStringId.SubmitPageMessageHtml); } else { message = this.GetFramesetMsg( FramesetStringId.SubmitPageMessageNoCurrentActivityHtml); } this.WriteSubmitPage(title, message, saveBtn); } else { // Requesting to move to a specific activity. The cmdData will include a numeric activity id. long activityId; if (long.TryParse(cmdData, out activityId)) { // If there is no current activity, or if any navigation requests did not // result in a move, then continue with the choice. if (!this.Session.HasCurrentActivity || !ProcessNavigationRequests(this.Session)) { if (this.Session.IsMoveToActivityValid(activityId)) { MoveToActivity(this.Session, activityId); this.ActivityHasChanged = true; this.LoadContentFrame = true; } } else { this.ActivityHasChanged = true; this.LoadContentFrame = true; } } } if (!this.ActivityHasChanged) { // Moving to the selected activity is not valid. It's possible that when the current // activity was unloaded, it exited or suspended itself. If that's the case, we // try to reactivate it. Note this causes the attempt count on the activity to // increment, so on "poorly" written content, the user may not see their data // anymore. this.ActivateCurrentActivity(); this.WriteError( ResHelper.Format( this.GetFramesetMsg(FramesetStringId.MoveToActivityFailedHtml), this.ThemeFolderPath), true); } this.mSaveOnly = false; } break; case Commands.DoIsChoiceValid: { string activityKey = cmdInfo.CommandData; bool isValid = false; if (!string.IsNullOrEmpty(activityKey)) { isValid = this.Session.IsMoveToActivityValid(activityKey); } this.mIsNavValidResponse = new IsNavValidResponseData( Commands.DoIsChoiceValid, activityKey, isValid); this.mSaveOnly = false; } break; case Commands.DoIsNavigationValid: { string navCommand = cmdInfo.CommandData; if (!string.IsNullOrEmpty(navCommand)) { bool isValid = false; if (navCommand == Commands.DoNext) { isValid = this.Session.IsMoveToNextValid(); } else if (navCommand == Commands.DoPrevious) { isValid = this.Session.IsMoveToPreviousValid(); } this.mIsNavValidResponse = new IsNavValidResponseData( Commands.DoIsNavigationValid, navCommand, isValid); } this.mSaveOnly = false; } break; case Commands.DoSave: { // Do nothing. The information will be saved since the page was posted. } break; case Commands.DoTerminate: { // end the current activity try { if (this.Session.View == SessionView.Execute) { // Keep track of state before calling ProcessNavigationRequests so that we can // detect if the call changes it in a way that requires reloading the content frame. long activityBeforeNavigation = this.Session.CurrentActivityId; int activityAttemptCount = this.Session.CurrentActivityDataModel.ActivityAttemptCount; LearningSession session = this.Session; if (session.HasCurrentActivity) { session.ProcessNavigationRequests(); } // The activity has changed if... // ... the session now does not have a current activity (since it did before we did the ProcessNavRequests call, OR // ... the session's current activity is not activity anymore (since it was before the call), OR // ... the session's activity id has changed // ... the session's activity id has not changed, but the attempt count has if (!session.HasCurrentActivity) { this.ActivityHasChanged = true; this.LoadContentFrame = true; } else if ((session.View == SessionView.Execute) && (!this.Session.CurrentActivityIsActive)) { // In Execute view, it started as active or would have thrown an exception. this.ActivityHasChanged = true; // do not load content frame, as that causes messages to flash while switching activities this.LoadContentFrame = false; } else if (activityBeforeNavigation != session.CurrentActivityId) { // The current activity has changed. this.ActivityHasChanged = true; this.LoadContentFrame = true; } else if ((activityBeforeNavigation == session.CurrentActivityId) && (activityAttemptCount != session.CurrentActivityDataModel.ActivityAttemptCount)) { // The activity has not changed, but the attempt count has. this.ActivityHasChanged = true; this.LoadContentFrame = true; } else { // In all other cases, it has not changed this.ActivityHasChanged = false; this.LoadContentFrame = false; } } else if ((this.Session.View == SessionView.Review) || (this.Session.View == SessionView.RandomAccess)) { // The activity has changed simply by calling this. This allows the client RTE to reinitialize and behave // as if navigation has happened. this.ActivityHasChanged = true; this.LoadContentFrame = true; } } catch (SequencingException ex) { this.WriteError( ResHelper.GetMessage( Localization.GetMessage("HID_TerminateFailed"), HttpUtility.HtmlEncode(ex.Message))); } this.mSaveOnly = false; } break; case Commands.DoSubmit: { // Submit the attempt -- meaning, do an ExitAll if (this.Session.View == SessionView.Execute) { if (this.Session.HasCurrentActivity) { ProcessNavigationRequests(this.Session); } this.Session.Exit(); } else if (this.Session.View == SessionView.RandomAccess) { // This may also be a request to end grading (for SLK), in which case // we just set a flag, get the right strings to display and call it done this.SessionIsEnded = true; processSessionEnd( this.Session, ref this.mSessionEndedMsgTitle, ref this.mSessionEndedMsg); } this.ActivityHasChanged = true; this.LoadContentFrame = true; this.mSaveOnly = false; } break; } } } else { // Is this the first page load, as part of frameset? (Some hidden values are only rendered in this case.) string param = this.Request.QueryString[FramesetQueryParameter.Init]; if (!string.IsNullOrEmpty(param)) { this.mIsFramesetInitialization = true; } } // If this was not simply a save operation and there is no current activity, then display a message // asking the user to select one. if (!this.mSaveOnly && !this.Session.HasCurrentActivity) { this.RegisterError( this.GetFramesetMsg(FramesetStringId.SelectActivityTitleHtml), this.GetFramesetMsg(FramesetStringId.SelectActivityMessageHtml), true); } // In Execute view, ProcessSessionEnd may write to the database and change state of data related to the attempt. // Therefore, session changes must be written in the same transation as the session end changes. TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.IsolationLevel = System.Transactions.IsolationLevel.Serializable; using (LearningStoreTransactionScope scope = new LearningStoreTransactionScope(transactionOptions)) { if (!this.SessionIsReadOnly) { // Save all changes this.Session.CommitChanges(); } if (this.Session.View == SessionView.Execute) { // The rollup and/or sequencing process may have changed the state of the attempt. If so, there are some cases // that cannot continue so show an error message. Allow the application to process this, since the messages are // different. processSessionEnd(this.Session, ref this.mSessionEndedMsgTitle, ref this.mSessionEndedMsg); } // finish the transaction scope.Complete(); } this.InitHiddenControlInfo(); this.mPageLoadSuccessful = true; } catch (ThreadAbortException) { // Do nothing -- thread is leaving. throw; } catch (Exception) { this.mPageLoadSuccessful = false; throw; } }
/// <summary> /// Gets the attempt id of the requested attempt. If the page is provided a learner assignment id and /// the attempt associated with the assignment doesn't exist, then one is created. If the user does not have /// access to the attempt, or the requested view of the attempt, or there is no /// attempt (such as non-elearning content) then false is returned. /// </summary> /// <param name="showErrorPage"></param> /// <param name="attemptId"></param> /// <returns></returns> public override bool TryGetAttemptId(bool showErrorPage, out AttemptItemIdentifier attemptId) { // Initialize out parameter attemptId = null; if (learnerAssignmentGuidIdHasBeenSet == false) { Guid learnerAssignmentGuidId; if (!TryProcessLearnerAssignmentIdParameter(showErrorPage, out learnerAssignmentGuidId)) { // In this case, if the parameter was not valid (eg, it's not a number), the error is already registered. // So just return. return(false); } LearnerAssignmentGuidId = learnerAssignmentGuidId; } // Put this operation in a transaction because if the attempt has not been started, we'll start the attempt // and update the assignment. Both should succeed or both should fail. LearnerAssignmentProperties la; TransactionOptions options = new TransactionOptions(); options.IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead; using (LearningStoreTransactionScope scope = new LearningStoreTransactionScope(options)) { // Must force a read of assignment data so that it's read in the same transaction that it might be updated. la = GetLearnerAssignment(true); SessionView view; if (!TryGetSessionView(true, out view)) { return(false); } if (la.Assignment.IsELearning) { // This is e-learning content m_isELearning = true; // Accessing LearnerAssignment (above) would have checked the database and retrieve any information available about // the assignment, including its attempt id, if it exists. If the LearnerAssignment information is valid // but there's no attempt, the AttemptId property will be null if (la.AttemptId == null) { // Only create the attempt if this is a request for Execute view if (view == SessionView.Execute) { if (!FileExistsInSharePoint(la.Assignment.Location)) { if (showErrorPage) { RegisterError(SlkFrameset.FRM_PackageNotFoundTitle, SlkFrameset.FRM_PackageNotFound, false); } return(false); } SlkStore.StartAttemptOnLearnerAssignment(LearnerAssignmentGuidId); // Force a reset of internal data regarding the current learner assignment la = GetLearnerAssignment(true); } else { // This is an error condition, since in other cases, the attempt must already exist. // Use private method to get the right error message. if (!ProcessViewRequest(la.Status, view)) { return(false); } } } else { // Attempt is already created. Verify the user has access to it and that the package exists. if (!ProcessViewRequest(la.Status, view)) { return(false); } if (!FileExistsInSharePoint(la.Assignment.Location)) { if (showErrorPage) { RegisterError(SlkFrameset.FRM_PackageNotFoundTitle, SlkFrameset.FRM_PackageNotFound, false); } return(false); } } // Attempt exists, set the out parameter attemptId = la.AttemptId; } else { // Is not e-learning assignment m_isELearning = false; // Verify that the learner can see the assignment. if (view == SessionView.Execute) { if (!FileExistsInSharePoint(la.Assignment.Location)) { if (showErrorPage) { RegisterError(SlkFrameset.FRM_PackageNotFoundTitle, SlkFrameset.FRM_PackageNotFound, false); } return(false); } // Mark the assignment as started if (la.Status != LearnerAssignmentState.Active) { la.Start(); } } else { // Verify this is a view they have access to given the state of the assignment. No need to check // return value, as non-elearning content always returns false from this method. if (!ProcessViewRequest(la.Status, view)) { return(false); } if (!FileExistsInSharePoint(la.Assignment.Location)) { if (showErrorPage) { RegisterError(SlkFrameset.FRM_PackageNotFoundTitle, SlkFrameset.FRM_PackageNotFound, false); } return(false); } } } scope.Complete(); } return(attemptId != null); }