Chapter 5 (State Machines) Samples
Listing 5.1. Sample Code for State Machine for Multiple-Choice Game
Option Explicit On Class MyStateMachineClass Private Enum GameState StartScreen AskQuestion CongratulateUser ScoldUser End Enum Private m_CurrentGameState As GameState '---------------------------------------------- 'The state machine that drives our user interface 'and manages other application state based on the 'mode the user is currently in '---------------------------------------------- Private Sub StateChangeForGame(ByVal newGameUIState _ As GameState) 'See which state the application is being brought into Select Case (newGameUIState) Case GameState.StartScreen 'If we are coming from a state that does not allow 'transition to this state, throw an exception If ((m_CurrentGameState <> GameState.CongratulateUser) _ AndAlso (m_CurrentGameState <> GameState.ScoldUser)) Then Throw New System.Exception("Illegal State Change!") End If 'UNDONE: Place code here to ' 1. Hide, Show, Move UI controls into place ' 2. Set up what ever variables/game-state needed ' for this mode ' ' SetUpGameStateForStartScreen() Case GameState.AskQuestion 'If we are coming from a state that does not allow 'transition to this state, throw an exception If ((m_CurrentGameState <> GameState.StartScreen) _ AndAlso (m_CurrentGameState <> GameState.CongratulateUser) _ AndAlso (m_CurrentGameState <> GameState.ScoldUser)) Then Throw New System.Exception("Illegal State Change!") End If 'UNDONE: Place code here to ' 1. Hide, Show, Move UI controls into place ' 2. Set up what ever variables/game-state needed ' for this mode ' ' SetUpGameStateForAskQuestion() Case GameState.CongratulateUser 'If we are coming from a state that does not allow 'transition to this state, throw an exception If (m_CurrentGameState <> GameState.AskQuestion) Then Throw New System.Exception("Illegal State Change!") End If 'UNDONE: Place code here to ' 1. Hide, Show, Move UI controls into place ' 2. Set up what ever variables/game-state needed ' for this mode ' ' SetUpGameStateForCongratulateUser() Case GameState.ScoldUser 'If we are coming from a state that does not allow 'transition to this state, throw an exception If (m_CurrentGameState <> GameState.AskQuestion) Then Throw New System.Exception("Illegal State Change!") End If 'UNDONE: Place code here to ' 1. Hide, Show, Move UI controls into place ' 2. Set up what ever variables/game-state needed ' for this mode ' ' SetUpGameStateForScoldUser() Case Else Throw New System.Exception("Unknown state!") End Select 'Store the new requested state as our current state m_CurrentGameState = newGameUIState End Sub End Class
Listing 5.2. Application State Being Changed Implicitly (Bad Design!)
'Code that gets run when the form is loaded Private Sub Form1_Load(ByVal sender As System.Object, ByVal _ e As System.EventArgs) Handles MyBase.Load TextBox1.Visible = True ListBox1.Visible = False End Sub 'Data Private m_someImportantInfo As String 'The user has clicked the button and wants to move on to the 'next step in this application. Hide the text box and show 'list box in its place. Private Sub Button1_Click(ByVal sender As System.Object, ByVal _ e As System.EventArgs) Handles Button1.Click m_someImportantInfo = TextBox1.Text TextBox1.Visible = False ListBox1.Visible = True End Sub
Listing 5.3. Application State Being Changed Explicitly (Good Design)
Private m_someImportantInfo As String 'Define the states the application can be in Enum MyStates step1 step2 End Enum '------------------------------------------------------ 'The central function that is called 'whenever the application state needs 'to be changed '------------------------------------------------------ Sub ChangeApplicationState(ByVal newState As MyStates) Select Case newState Case MyStates.step1 TextBox1.Visible = True ListBox1.Visible = False Case MyStates.step2 m_someImportantInfo = TextBox1.Text TextBox1.Visible = False ListBox1.Visible = True End Select End Sub '------------------------------------------------------ 'The user has clicked the button and wants to move on to the 'next step in this application. Hide the text box and show 'list box in its place. '------------------------------------------------------ Private Sub button1_Click(ByVal sender As Object, ByVal _ e As System.EventArgs) 'Call a central function to change the state ChangeApplicationState(MyStates.step2) End Sub '------------------------------------------------------ 'Code that gets run when the form is loaded '------------------------------------------------------ Private Sub Form1_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) 'Call a central function to change the state ChangeApplicationState(MyStates.step1) End Sub
Listing 5.4. Code for the Background Thread Prime Number Calculator
Option Strict On Imports System Public Class FindNextPrimeNumber 'States we can be in. Public Enum ProcessingState notYetStarted waitingToStartAsync lookingForPrime foundPrime requestAbort aborted End Enum Private m_startTickCount As Integer Private m_endTickCount As Integer Private m_startPoint As Long Private m_NextHighestPrime As Long Private m_processingState As ProcessingState '-------------------------------------------------- 'A very simple state machine. '-------------------------------------------------- Public Sub setProcessingState(ByVal nextState _ As ProcessingState) '-------------------------------------------------- 'Some very simple protective code to make sure 'we can't enter another state if we have either 'successfully finished, or successfully aborted '-------------------------------------------------- Dim currentState As ProcessingState currentState = getProcessingState() If ((currentState = ProcessingState.aborted) _ OrElse (currentState = ProcessingState.foundPrime)) Then Return End If 'Thread concurrency protection SyncLock (Me) 'Allow the state change m_processingState = nextState End SyncLock End Sub Public Function getProcessingState() As ProcessingState Dim currentState As ProcessingState 'Thread concurrency protection SyncLock (Me) currentState = m_processingState End SyncLock Return currentState End Function Public Function getTickCountDelta() As Integer If (getProcessingState() = _ ProcessingState.lookingForPrime) Then Throw New Exception( _ "Still looking for prime! No final time calculated") End If Return m_endTickCount - m_startTickCount End Function '--------------------------------------------------- 'Returns the prime '--------------------------------------------------- Public Function getPrime() As Long If (getProcessingState() <> ProcessingState.foundPrime) Then Throw New Exception("Prime number not calculated yet!") End If Return m_NextHighestPrime End Function 'Class constructor Public Sub New(ByVal startPoint As Long) setProcessingState(ProcessingState.notYetStarted) m_startPoint = startPoint End Sub '------------------------------------------------------------ 'Creates a new worker thread that will call ' "findNextHighestPrime()" '------------------------------------------------------------ Public Sub findNextHighestPrime_Async() Dim threadStart As System.Threading.ThreadStart threadStart = _ New System.Threading.ThreadStart( _ AddressOf findNextHighestPrime) Dim newThread As System.Threading.Thread newThread = New System.Threading.Thread(threadStart) 'Set our processing state to say that we are looking setProcessingState(ProcessingState.waitingToStartAsync) newThread.Start() End Sub '------------------------------------------------------------ 'This is the main worker function. This synchronously starts 'looking for the next prime number and does not exit until 'either: ' (a) The next prime is found ' (b) An external thread to this thread tells us to abort '------------------------------------------------------------ Public Sub findNextHighestPrime() 'If we've been told to abort, don't even start looking If (getProcessingState() = ProcessingState.requestAbort) Then GoTo finished_looking End If 'Set our processing state to say that we are looking setProcessingState(ProcessingState.lookingForPrime) m_startTickCount = System.Environment.TickCount Dim currentItem As Long 'See if it's odd If ((m_startPoint And 1) = 1) Then 'It's odd, start at the next odd number currentItem = m_startPoint + 2 Else 'It's even, start at the next odd number currentItem = m_startPoint + 1 End If 'Look for the prime item. While (getProcessingState() = ProcessingState.lookingForPrime) 'If we found the prime item, return it If (isItemPrime(currentItem) = True) Then m_NextHighestPrime = currentItem 'Update our state setProcessingState(ProcessingState.foundPrime) End If currentItem = currentItem + 2 End While finished_looking: 'Exit. At this point we have either been 'told to abort the search by another thread, or 'we have found and recorded the next highest prime number 'Record the time m_endTickCount = System.Environment.TickCount 'If an abort was requested, note that we have aborted 'the process. If (getProcessingState() = ProcessingState.requestAbort) Then setProcessingState(ProcessingState.aborted) End If End Sub 'Helper function that looks to see if a specific item 'is a prime number. Private Function isItemPrime(ByVal potentialPrime _ As Long) As Boolean 'If it's even, it's not prime If ((potentialPrime And 1) = 0) Then Return False End If 'We want to look up until just past the square root 'of the item Dim end_point_of_search As Long end_point_of_search = _ CLng(System.Math.Sqrt(potentialPrime) + 1) Dim current_test_item As Long = 3 While (current_test_item <= end_point_of_search) '--------------------------------- 'Check to make sure we have not been asked to abort! '--------------------------------- If (getProcessingState() <> _ ProcessingState.lookingForPrime) Then Return False End If 'If the item is divisible without remainder, 'it is not prime If (potentialPrime Mod current_test_item = 0) Then Return False End If 'advance by two current_test_item = current_test_item + 2 End While 'The item is prime Return True End Function End Class
Listing 5.5. Test Code to Call the Background Thread Prime Number Calculator Above
'------------------------------------------------------------ 'Code to process the Click event of Button1 on the form ' 'Call the prime number function on this thread! '(This will block the thread) '------------------------------------------------------------ Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Dim testItem As Long testItem = System.Convert.ToInt64("123456789012345") Dim nextPrimeFinder As FindNextPrimeNumber nextPrimeFinder = New FindNextPrimeNumber(testItem) nextPrimeFinder.findNextHighestPrime() Dim nextHighestPrime As Long nextHighestPrime = nextPrimeFinder.getPrime() MsgBox(CStr(nextHighestPrime)) 'How long did the caclulation take? Dim calculation_time As Integer calculation_time = nextPrimeFinder.getTickCountDelta() MsgBox(CStr(calculation_time) + " ms") End Sub '------------------------------------------------------------ 'Code to process the Click event of Button2 on the form ' 'Call the prime number function on another thread! '(This will not 'block this thread) 'We will use a state machine to track the progress '------------------------------------------------------------ Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button2.Click Dim testItem As Long testItem = System.Convert.ToInt64("123456789012345") Dim nextPrimeFinder As FindNextPrimeNumber nextPrimeFinder = New FindNextPrimeNumber(testItem) '------------------------------------ 'Do the processing on another thread '------------------------------------ nextPrimeFinder.findNextHighestPrime_Async() 'Go in a loop and wait until we either find the result 'or it is aborted While ((nextPrimeFinder.getProcessingState() <> _ FindNextPrimeNumber.ProcessingState.foundPrime) _ And _ (nextPrimeFinder.getProcessingState() <> _ FindNextPrimeNumber.ProcessingState.aborted)) 'TEST CODE ONLY: 'Show a message box, allow the user to dismiss it 'This will help pass the time! MsgBox("Still Looking...Click OK") 'We could abort the search by calling: 'nextPrimeFinder.setProcessingState( ' FindNextPrimeNumber.ProcessingState.requestAbort) End While 'If we aborted the search, exit gracefully If (nextPrimeFinder.getProcessingState() = _ FindNextPrimeNumber.ProcessingState.aborted) Then MsgBox("Search was aborted!") Return End If Dim nextHighestPrime As Long nextHighestPrime = nextPrimeFinder.getPrime() MsgBox(CStr(nextHighestPrime)) 'How long did the calculation take? Dim calculation_time As Integer calculation_time = nextPrimeFinder.getTickCountDelta() MsgBox(CStr(calculation_time) + " ms") End Sub
|