Машина состояний Web-приложения
Разумеется, все программирование - это смена состояний конечного автомата - программы, которе может быть позиционное - испольняемый номер строки программы или по состояниям, отмеченных особыми флагами состояния. В виндузовых приложениях - все проще - в зависимости от действий пользователя почти всегда выполняются РАЗНЫЕ участки программы, однако в ВЭБ-приложениях все самое интересное происходит всегда в одном и том же участке программы - Page_Load. И вся сложная логика сосредотачивается в этом месте, размер которого лишь в исключительно редких случаях бывает менее 1000 строк.
На этой страничке мы рассмотрим полный пример построения странички с достаточно сложной схемой смены состояний.
Проектирование такой странички начинается всегда с описания (желательно формализованного) всех входных параметров работы. Эти параметры надо тщательнейшим образом проанализировать на входе - и определить РЕЖИМ работы страницы, заданный входными параметрами.
00001: Imports System.Web 00002: Imports System.Web.UI 00003: Imports System.Data, System.Configuration 00004: Imports SALWeb.Courses.BusinessLogicLayer 00005: 00006: Partial Class Courses_Default 00007: Inherits System.Web.UI.Page 00008: 00009: ''' <summary> 00010: ''' This is a input mode of calling this page 00011: ''' </summary> 00012: Private Enum InputMode 00013: SemesterAndCourse = 1 00014: SemesterAndCourseWithMainSub = 2 00015: SemesterAndLector = 3 00016: SemesterAndLectorWithMainSub = 4 00017: Semester = 5 00018: SemesterWithMainSub = 6 00019: Sommerkurse = 7 00020: SommerKurseWithMainSub = 8 00021: Lector = 9 00022: LectorWithMainSub = 10 00023: Course = 11 00024: CourseWithMainSub = 12 00025: Samstags = 13 00026: SamstagsWithMainSub = 14 00027: SamstagsAndCourceWithMainSub = 14 00028: SamstagsAndCource = 15 00029: None = 16 00030: MainSub = 17 00031: End Enum 00032: 00033: ''' <summary> 00034: ''' This is a main input parameters in this page state machine 00035: ''' </summary> 00036: Private Enum PageState 00037: Prepare = 1 00038: SemesterPostBack = 2 00039: MainPostBack = 3 00040: SubPostBack = 4 00041: OrderPostBack = 5 00042: Init = 6 00043: End Enum 00044: 00045: ''' <summary> 00046: ''' This Function ONLY determine input parm and return Mode of running this page 00047: ''' </summary> 00048: Private Function GetInputMode() As InputMode 00049: Select Case Request.QueryString("ID") 00050: Case "", 1 00051: If Request.QueryString("SID") <> "" Then 00052: 'семестр задан 00053: If Request.QueryString("CID") <> "" Then 00054: 'задан семестр+номер курса 00055: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00056: 'с уточненной фильтрацией 00057: Return InputMode.SemesterAndCourseWithMainSub 00058: Else 00059: Return InputMode.SemesterAndCourse 00060: End If 00061: Else 00062: If Request.QueryString("LecturerID") <> "" Then 00063: 'задан семестр+лектор 00064: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00065: 'с уточненной фильтрацией 00066: Return InputMode.SemesterAndLectorWithMainSub 00067: Else 00068: Return InputMode.SemesterAndLector 00069: End If 00070: Else 00071: 'задан только семестр 00072: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00073: 'с уточненной фильтрацией 00074: Return InputMode.SemesterWithMainSub 00075: Else 00076: Return InputMode.Semester 00077: End If 00078: End If 00079: End If 00080: Else 00081: 'семестра нету 00082: If Request.QueryString("LecturerID") <> "" Then 00083: 'задан только лектор 00084: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00085: 'с уточненной фильтрацией 00086: Return InputMode.LectorWithMainSub 00087: Else 00088: Return InputMode.Lector 00089: End If 00090: Else 00091: If Request.QueryString("CID") <> "" Then 00092: 'задан номер курса 00093: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00094: 'с уточненной фильтрацией 00095: Return InputMode.CourseWithMainSub 00096: Else 00097: Return InputMode.Course 00098: End If 00099: Else 00100: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00101: 'с уточненной фильтрацией 00102: Return InputMode.MainSub 00103: Else 00104: 'ничего не задано вообще 00105: Return InputMode.None 00106: End If 00107: 00108: End If 00109: End If 00110: End If 00111: Case 2 00112: 'Samstags-Seminare 00113: If Request.QueryString("CID") = "" Then 00114: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00115: 'с уточненной фильтрацией 00116: Return InputMode.SamstagsWithMainSub 00117: Else 00118: Return InputMode.Samstags 00119: End If 00120: Else 00121: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00122: 'с уточненной фильтрацией 00123: Return InputMode.SamstagsAndCourceWithMainSub 00124: Else 00125: Return InputMode.SamstagsAndCource 00126: End If 00127: End If 00128: Case 3 00129: 'Sommerkurse 00130: If Request.QueryString("MainID") <> "" And Request.QueryString("SubID") <> "" Then 00131: 'с уточненной фильтрацией 00132: Return InputMode.SommerKurseWithMainSub 00133: Else 00134: Return InputMode.Sommerkurse 00135: End If 00136: Case Else 00137: Return InputMode.None 00138: End Select 00139: End Function 00140: 00141: ''' <summary> 00142: ''' This sub reading Input parameters for page and set parameters for execurion BLL 00143: ''' </summary> 00144: Private Sub GetInputParm(ByVal InputState As InputMode) 00145: Select Case InputState 00146: Case InputMode.Semester 00147: Session("CourseListMode") = Course_Mode.FillBySemester 00148: Session("SemesterID") = Request.QueryString("SID") 00149: Case InputMode.SemesterWithMainSub 00150: Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00151: Session("SemesterID") = Request.QueryString("SID") 00152: Session("MainCategoryID") = Request.QueryString("MainID") 00153: Session("SubCategoryID") = Request.QueryString("SubID") 00154: Case InputMode.SemesterAndCourse 00155: Session("CourseListMode") = Course_Mode.FillByNummerAndSemester 00156: Session("SemesterID") = Request.QueryString("SID") 00157: Session("CourseID") = Request.QueryString("CID") 00158: Case InputMode.SemesterAndCourseWithMainSub 00159: Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00160: Session("SemesterID") = Request.QueryString("SID") 00161: Session("CourseID") = Request.QueryString("CID") 00162: Session("MainCategoryID") = Request.QueryString("MainID") 00163: Session("SubCategoryID") = Request.QueryString("SubID") 00164: Case InputMode.Course 00165: Session("CourseListMode") = Course_Mode.FillByNummer 00166: Session("CourseID") = Request.QueryString("CID") 00167: Case InputMode.CourseWithMainSub 00168: Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00169: Session("CourseID") = Request.QueryString("CID") 00170: Session("MainCategoryID") = Request.QueryString("MainID") 00171: Session("SubCategoryID") = Request.QueryString("SubID") 00172: Case InputMode.SemesterAndLector 00173: Session("CourseListMode") = Course_Mode.FillBySemesterAndLector 00174: Session("SemesterID") = Request.QueryString("SID") 00175: Session("LectorID") = Request.QueryString("LecturerID") 00176: Case InputMode.SemesterAndLectorWithMainSub 00177: Session("CourseListMode") = Course_Mode.FillBySemesterAndLectorAndMainSubCategory 00178: Session("SemesterID") = Request.QueryString("SID") 00179: Session("LectorID") = Request.QueryString("LecturerID") 00180: Session("MainCategoryID") = Request.QueryString("MainID") 00181: Session("SubCategoryID") = Request.QueryString("SubID") 00182: Case InputMode.LectorWithMainSub 00183: Session("CourseListMode") = Course_Mode.FillBySemesterAndLectorAndMainSubCategory 00184: Session("LectorID") = Request.QueryString("LecturerID") 00185: Session("MainCategoryID") = Request.QueryString("MainID") 00186: Session("SubCategoryID") = Request.QueryString("SubID") 00187: 'а Session("SemesterID") возьмется из комбешника или по умолчанию 00188: Case InputMode.Lector 00189: Session("CourseListMode") = Course_Mode.FillByLector 00190: Session("LectorID") = Request.QueryString("LecturerID") 00191: Case InputMode.Sommerkurse 00192: Session("CourseListMode") = Course_Mode.FillBySemester 00193: 'а Session("SemesterID") возьмется из комбешника или по умолчанию 00194: Case InputMode.SommerKurseWithMainSub 00195: Session("MainCategoryID") = Request.QueryString("MainID") 00196: Session("SubCategoryID") = Request.QueryString("SubID") 00197: 'а Session("SemesterID") возьмется из комбешника или по умолчанию 00198: Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00199: Case InputMode.Samstags 00200: Session("CourseListMode") = Course_Mode.FillSamBySemester 00201: 'а Session("SemesterID") возьмется из комбешника или по умолчанию 00202: Case InputMode.SamstagsWithMainSub 00203: Session("CourseListMode") = Course_Mode.FillSamBySemesterAndMainSubCategory 00204: Session("MainCategoryID") = Request.QueryString("MainID") 00205: Session("SubCategoryID") = Request.QueryString("SubID") 00206: Case InputMode.SamstagsAndCource, InputMode.SamstagsAndCourceWithMainSub 00207: Session("CourseListMode") = Course_Mode.FillSamBySemesterAndNummer 00208: Session("CourseID") = Request.QueryString("CID") 00209: Case InputMode.MainSub 00210: Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00211: Session("MainCategoryID") = Request.QueryString("MainID") 00212: Session("SubCategoryID") = Request.QueryString("SubID") 00213: 'а Session("SemesterID") возьмется из комбешника или по умолчанию 00214: Case InputMode.None 00215: Session("CourseListMode") = Course_Mode.FillBySemester 00216: End Select 00217: End Sub
Как видите, у этой странички - 17 различных режимов работы, заданных входными параметрами. Но сложность тут в том, что на страничке существует еще четыре комбешника которые ИНОГДА меняют режим работы странички, переводя ее например из режима отбора только курсов в разрезе семестров, в разрез просмотра курсов по подкатегориям.
Кроме того, страничка имеет больше десятка меток, некоторое из которых показываются в одних состояних, некотороые в других. Грубо говоря, эта страничка имеет 10 вот таких фрагментов (из которых показаны только три):
00220: ''' <summary> 00221: ''' This Sub ONLY show lblCourseCount 00222: ''' </summary> 00223: Private Sub ShowLabelCount(ByVal InputState As InputMode) 00224: If Session("CourseListCount") Is Nothing Then 00225: lblCourseCount.Visible = False 00226: Else 00227: lblCourseCount.Visible = True 00228: Select Case InputState 00229: Case InputMode.Samstags, InputMode.SamstagsWithMainSub 00230: If Session("CourseListCount") = 1 Then 00231: lblCourseCount.Text = "Es wurde <b>1</b> Samstags-Seminar gefunden." 00232: ElseIf Session("CourseListCount") = 0 Then 00233: lblCourseCount.Text = "Es wurde kein Samstags-Seminar gefunden." 00234: Else 00235: lblCourseCount.Text = "Es wurden <b>" & Session("CourseListCount") & "</b> Samstags-Seminare gefunden." 00236: End If 00237: Case InputMode.Sommerkurse, InputMode.SommerKurseWithMainSub 00238: If Session("CourseListCount") = 1 Then 00239: lblCourseCount.Text = "Es wurde <b>1</b> Sommerkurse gefunden." 00240: ElseIf Session("CourseListCount") = 0 Then 00241: lblCourseCount.Text = "Es wurde kein Sommerkurse gefunden." 00242: Else 00243: lblCourseCount.Text = "Es wurden <b>" & Session("CourseListCount") & "</b> Sommerkurse gefunden." 00244: End If 00245: Case InputMode.SemesterAndCourse, InputMode.SemesterAndLector, InputMode.Semester, InputMode.Course, InputMode.Lector, InputMode.CourseWithMainSub, InputMode.LectorWithMainSub, InputMode.SemesterAndCourseWithMainSub, InputMode.SemesterWithMainSub, InputMode.SemesterAndLectorWithMainSub, InputMode.MainSub, InputMode.None 00246: If Session("CourseListCount") = 1 Then 00247: lblCourseCount.Text = "Es wurde <b>1</b> Kurs gefunden." 00248: ElseIf Session("CourseListCount") = 0 Then 00249: lblCourseCount.Text = "Es wurde kein Kurs gefunden." 00250: Else 00251: lblCourseCount.Text = "Es wurden <b>" & Session("CourseListCount") & "</b> Kurse gefunden." 00252: End If 00253: End Select 00254: End If 00255: End Sub 00256: 00257: ''' <summary> 00258: ''' This sub ONLY show lblChoice 00259: ''' </summary> 00260: Private Sub ShowlblChoice(ByVal PageStep As PageState, ByVal InputState As InputMode) 00261: lblChoice.Visible = True 00262: 'Напрямую из комбешников значения взять не получается. Они еще не привязаны и не загружены. 00263: Dim OneMainRow As DataRowView 00264: For Each OneMainRow In courseMainObj.Select 00265: If OneMainRow(0).ToString = Session("MainCategoryID") Then Exit For 00266: Next 00267: Dim OneSubRow As DataRowView 00268: For Each OneSubRow In courseSubObj.Select 00269: If OneSubRow(0).ToString = Session("SubCategoryID") Then Exit For 00270: Next 00271: ' 00272: Select Case InputState 00273: Case InputMode.SemesterAndLector, InputMode.Lector, InputMode.LectorWithMainSub, InputMode.SemesterAndLectorWithMainSub 00274: lblChoice.Text = Request.QueryString("Firstname") & " " & Request.QueryString("Name") & " Kurse an der SAL:" 00275: 'Select Case Session("CourseListCount") 00276: ' Case 0 : lblChoice.Text &= "has no cources an der SAL" 00277: ' Case 1 : lblChoice.Text &= "has one cources an der SAL" 00278: ' Case Else : lblChoice.Text &= "unterrichtet folgende Kurse an der SAL" 00279: 'End Select 00280: Case InputMode.Sommerkurse, InputMode.Semester, InputMode.SemesterAndCourse, InputMode.Course, InputMode.CourseWithMainSub, InputMode.SamstagsWithMainSub, InputMode.SemesterAndCourseWithMainSub, InputMode.SemesterWithMainSub, InputMode.MainSub 00281: Select Case PageStep 00282: Case PageState.MainPostBack 00283: If Session("ddlMainIndex") <> 0 Then 00284: lblChoice.Text = "Sie haben <b>" & OneMainRow(1) & "</b> gewдhlt." 00285: Else 00286: lblChoice.Text = "" 00287: End If 00288: Case PageState.SubPostBack 00289: If Session("ddlSubIndex") <> 0 Then 00290: lblChoice.Text = "Sie haben <b>" & OneMainRow(1) & "</b> <img src=""images/layout/arrow_intro.gif"" border=""0"" with=""14"" height=""14""> <b> " + OneSubRow(1) & "</b> gewдhlt." 00291: Else 00292: lblChoice.Text = "Sie haben <b>" & OneMainRow(1) & "</b> gewдhlt." 00293: End If 00294: End Select 00295: Case InputMode.Samstags, InputMode.SamstagsWithMainSub, InputMode.SamstagsAndCource, InputMode.SamstagsAndCourceWithMainSub 00296: If Session("CourseListCount") = 1 Then 00297: lblChoice.Text = "Es werden <b>alle Kurse</b> angezeigt." 00298: ElseIf Session("CourseListCount") = 2 Then 00299: lblChoice.Text = "Es werden <b>alle Samstags-Seminare</b> angezeigt." 00300: End If 00301: End Select 00302: End Sub 00303: 00304: ''' <summary> 00305: ''' This Sub ONLY show main Title on the page 00306: ''' </summary> 00307: Private Sub ShowMainTitle(ByVal InputState As InputMode) 00308: Dim dvSemester As DataView = SemesterObj.Select 00309: Dim dtSemester As New DataTable 00310: dtSemester = dvSemester.ToTable 00311: Dim dvwSemester As DataView = dtSemester.DefaultView 00312: dvwSemester.RowFilter = "SemesterID = '" & Session("SemesterID") & "'" 00313: Dim i As Integer = dvwSemester.Count 00314: Dim mySemesterBeginn As DateTime = Nothing 00315: Dim mySemesterEnde As DateTime = Nothing 00316: If i < 1 Then 00317: lblKursverzeichnisTitel.Text = "Achtung: Kurs ist nicht mehr aktuell." 00318: Exit Sub 00319: End If 00320: Dim mySemesterTitle As String = CType(dvwSemester(0)("Semester_lang"), String) 00321: If Not IsDBNull(dvwSemester(0)("Beginn")) Then mySemesterBeginn = dvwSemester(0)("Beginn") 00322: If Not IsDBNull(dvwSemester(0)("Ende")) Then mySemesterEnde = dvwSemester(0)("Ende") 00323: ' 00324: lblTimespan.Text = Format(mySemesterBeginn, "dd.MM.yy") & " - " & Format(mySemesterEnde, "dd.MM.yy") 00325: ' 00326: Select Case InputState 00327: Case InputMode.SemesterAndCourse, InputMode.SemesterAndLector, InputMode.Semester, InputMode.Course, InputMode.Lector, InputMode.CourseWithMainSub, InputMode.LectorWithMainSub, InputMode.SemesterAndCourseWithMainSub, InputMode.SemesterAndLectorWithMainSub, InputMode.SemesterWithMainSub, InputMode.None, InputMode.MainSub 00328: lblKursverzeichnisTitel.Text = "Kursverzeichnis " & mySemesterTitle 00329: Case InputMode.Samstags, InputMode.SamstagsWithMainSub, InputMode.SamstagsAndCource, InputMode.SamstagsAndCourceWithMainSub 00330: lblKursverzeichnisTitel.Text = "Samstags-Seminars" 00331: 'lblTimespan.Text = "Die Sommerkure fьr den kommenden Sommer sind noch nicht publiziert." 00332: Case InputMode.Sommerkurse, InputMode.SommerKurseWithMainSub 00333: lblKursverzeichnisTitel.Text = "Sommerkurse" 00334: 'lblTimespan.Text = "Die Sommerkure fьr den kommenden Sommer sind noch nicht publiziert." 00335: End Select 00336: End Sub
Вот такая машина состояний - это настоящая игра ума для программиста! Как же решать такие задачи?
Для начала надо определится, КАК именно постбеки от комбешников будут менять входные параметры. Это ключ ко всей этой проблематике. Решите эту задачу - все остальное пойдет как по маслу. В данной страничке я решил этот вопрос так:
И если это ключевой момент вам удасться решить правильно, то дальше все пойдет как по маслу и Page_Load у вас должен начаться примерно так:
00700: Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 00701: Dim InputState As InputMode 00702: Dim PageStep As PageState 00703: If Session("PageStep") Is Nothing Then Session("PageStep") = PageState.Prepare 00704: If IsPostBack Then 00705: 'от комбешников - ничего не делаем - пролетаем сквозняком на SelectedIndexChanged 00706: Else 00707: 'состояния комбешников и ViewState потерялось
И далее, следует собственно основной код Page_Load (который конечно вряд-ли уложится в одну-две тысячи строк в такой страничке), но выглядеть он будет вполне удобоваримо и однообразно.
00720: Case PageState.SubPostBack : RestoreComboState() : GetInputParm(InputState) 00721: Select Case InputState 00722: Case InputMode.None : Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00723: Case InputMode.Course : Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00724: Case InputMode.Lector : Session("CourseListMode") = Course_Mode.FillBySemesterAndLectorAndMainSubCategory 00724: Case InputMode.Samstags : Session("CourseListMode") = Course_Mode.FillSamBySemesterAndMainSubCategory 00726: Case InputMode.Sommerkurse : Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00727: Case InputMode.Semester : Session("CourseListMode") = Course_Mode.FillBySemesterAndMainSubCategry 00728: End Select 00729: ShowCourseList(InputState) 00730: ShowSemesterddl() 00731: ShowSubCategoryddl(InputState, PageStep) 00732: ShowlblChoice(PageStep, InputState) 00733: If Session("ddlSubIndex") <> 0 Then 00734: ShowOrderByddl() 00735: Else 00736: ddlCourseOrderBy.Visible = False 00737: lblCourseOrderBy.Visible = False 00738: lblCourseCount.Visible = False 00739: repCourses.Visible = False 00740: End If 00741: ShowLabelCount(InputState) 00742: Session("PageStep") = PageState.Init
На мой взгляд только такая структура странички позволяет вообще как-то понять ее работу и реально вносить в нее изменения. Только так можно удовлетворять в приемлимые сроки требования заказчиков на модификации машины состояний странички.
Заметьте, что в соответствии со своей классификацией машин состояний - это распределенная машина состояний. Меня посещала мысль сделать на этой страничке централлизованную машину состояний - она еще легче и быстрее потом модицифирума при появлении изменений. И еще лучше было бы хранить ее в SQL и сделать админку к этой машине состояний - но в этом проекте были слишком сжатые сроки и я просто не успел это сделать. В таком случае все-все-все вот эти флажки для отображения меток и переходов из режима в режим можно было бы хранить в базе. Тогда заказчик сам бы по ходу дела мог бы определять, например, при переходе в режим лектора (отображения курсов по заданному лектору) - шелчки по комбешникам основых/дополнительных курсов - переводят ли странику в режим отбора курсов просто по категориям, или же она остается в режиме лектора и отображает курсы только для заданного лектора с уточнением читаемых им курсов по категориям.
|