(ASP.NET) ASP.NET (2007 год)

Машина состояний 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 и сделать админку к этой машине состояний - но в этом проекте были слишком сжатые сроки и я просто не успел это сделать. В таком случае все-все-все вот эти флажки для отображения меток и переходов из режима в режим можно было бы хранить в базе. Тогда заказчик сам бы по ходу дела мог бы определять, например, при переходе в режим лектора (отображения курсов по заданному лектору) - шелчки по комбешникам основых/дополнительных курсов - переводят ли странику в режим отбора курсов просто по категориям, или же она остается в режиме лектора и отображает курсы только для заданного лектора с уточнением читаемых им курсов по категориям.



Comments ( )
Link to this page: //www.vb-net.com/asp2/26/index.htm
< THANKS ME>