void StageChanged(int value)
        {
            testSpec.Stage = value;

            if (testSpec.Stage < 7)
            {
                testSpec.TenorWeight = 8;
            }
            else if (testSpec.Stage > 8)
            {
                testSpec.TenorWeight = 23;
            }

            testSpec.TenorWeightDisabled = TenorWeightSelect.TenorWeightDisabled(testSpec.Stage);

            if (blowSetA != null)
            {
                blowSetA = null;
            }

            if (blowSetB != null)
            {
                blowSetB = null;
            }
        }
        void TestBellLocChanged(int value)
        {
            testSpec.TestBellLoc = value;

            if (blowSet != null)
            {
                blowSet = null;
            }
        }
        void ErrorSizeChanged(int value)
        {
            testSpec.ErrorSize = value;

            if (blowSet != null)
            {
                blowSet = null;
            }
        }
        void NumRowsChanged(int value)
        {
            testSpec.NumRows = value;

            if (blowSet != null)
            {
                blowSet = null;
            }
        }
        void TenorWeightChanged(int value)
        {
            testSpec.TenorWeight = value;

            if (blowSet != null)
            {
                blowSet = null;
            }
        }
        async Task TestChanged(int value)
        {
            testSpec.SelectedTest = value;
            blowSet = null;

            if (testSpec.SelectedTest != 0 && testSpec.SelectedTest != -1)
            {
                await Load(testSpec.SelectedTest);
            }
        }
        async Task Load(int id)
        {
            // Get a test from the API
            ABTestData aBTestData = await TJBarnesService.GetHttpClient()
                                    .GetFromJsonAsync <ABTestData>("api/abtests/" + id.ToString());

            // Use the Deserializer method of the JsonSerializer class (in the System.Text.Json namespace) to create
            // a BlowSetCore object for each of A and B
            BlowSetCore blowSetCoreA = JsonSerializer.Deserialize <BlowSetCore>(aBTestData.ABTestSpecA);
            BlowSetCore blowSetCoreB = JsonSerializer.Deserialize <BlowSetCore>(aBTestData.ABTestSpecB);

            testSpec.AHasErrors = blowSetCoreA.HasErrors;

            // Create a BlowSet object from the BlowSetCore object for A
            blowSetA = new BlowSet(blowSetCoreA.Stage, blowSetCoreA.NumRows, blowSetCoreA.TenorWeight,
                                   blowSetCoreA.ErrorType, blowSetCoreA.HasErrors);

            // Use an audioIdSuffix of "b" for this blowset
            blowSetA.LoadBlows(blowSetCoreA, "a");

            blowSetA.SetUnstruck();

            // Create a BlowSet object from the BlowSetCore object for B
            blowSetB = new BlowSet(blowSetCoreB.Stage, blowSetCoreB.NumRows, blowSetCoreB.TenorWeight,
                                   blowSetCoreB.ErrorType, blowSetCoreB.HasErrors);

            // Use an audioIdSuffix of "b" for this blowset
            blowSetB.LoadBlows(blowSetCoreB, "b");

            blowSetB.SetUnstruck();

            // Update drop down boxes on screen
            // Use BlowSetA - by definition the following properties for BlowSetA and BlowSetB will be the same
            testSpec.Stage       = blowSetA.Stage;
            testSpec.TenorWeight = blowSetA.TenorWeight;
            testSpec.ErrorType   = blowSetA.ErrorType;

            // Set up test spec-dependent elements of the screen object
            int baseGap = BaseGaps.BaseGap(testSpec.Stage, testSpec.TenorWeight, 1);

            testSpec.BaseGap = baseGap;

            // Set the timing for the animation (when not showing the bells)
            screenA.AnimationDuration = blowSetA.Blows.Last().GapCumulative + 1000;
            screenB.AnimationDuration = blowSetB.Blows.Last().GapCumulative + 1000;
            testSpec.ResultEntered    = false;

            testSpec.ShowGaps = false;
            StateHasChanged();
        }
        void ErrorTypeChanged(int value)
        {
            testSpec.ErrorType = value;

            if (blowSetA != null)
            {
                blowSetA = null;
            }

            if (blowSetB != null)
            {
                blowSetB = null;
            }
        }
        async Task Load(int id)
        {
            // Get a test from the API
            GapTestData gapTestData = await TJBarnesService.GetHttpClient().
                                      GetFromJsonAsync <GapTestData>("api/gaptests/" + id.ToString());

            // Use the Deserializer method of the JsonSerializer class (in the System.Text.Json namespace) to create
            // a BlowSetCore object
            BlowSetCore blowSetCore = JsonSerializer.Deserialize <BlowSetCore>(gapTestData.GapTestSpec);

            // Now create a BlowSet object from the BlowSetCore object
            blowSet = new BlowSet(blowSetCore.Stage, blowSetCore.NumRows, blowSetCore.TenorWeight,
                                  blowSetCore.ErrorType, true);

            // No need for an audio suffix in a Gap test (this is used to distinguish A and B in an A/B test)
            blowSet.LoadBlows(blowSetCore, string.Empty);
            blowSet.SetUnstruck();

            // Update drop down boxes on screen
            testSpec.Stage       = blowSet.Stage;
            testSpec.TenorWeight = blowSet.TenorWeight;
            testSpec.NumRows     = blowSet.NumRows;

            // Set up test spec-dependent elements of the screen object
            int baseGap = BaseGaps.BaseGap(testSpec.Stage, testSpec.TenorWeight, 1);

            testSpec.BaseGap = baseGap;
            testSpec.GapMin  = 20;

            // If test bell is 1st's place of a handstroke row, need to adjust GapMax to have a higher value
            // because of the handstroke gap
            if (blowSet.Blows.Last().IsHandstroke == true && blowSet.Blows.Last().Place == 1)
            {
                testSpec.GapMax = Convert.ToInt32(Math.Round(((double)testSpec.BaseGap * 3) / 50)) * 50;
            }
            else
            {
                testSpec.GapMax = Convert.ToInt32(Math.Round(((double)baseGap * 2) / 50)) * 50;
            }

            testSpec.ShowGaps = false;
            StateHasChanged();
        }
        void Create()
        {
            Block testBlock = new Block(testSpec.Stage, testSpec.NumRows);

            testBlock.CreateRandomBlock();

            // Set place to be the test place
            int testPlace;

            testPlace = testSpec.Stage + (testSpec.Stage % 2);

            if (testSpec.TestBellLoc != 1)
            {
                Random rand = new Random();
                testPlace = rand.Next(1, testPlace + 1);
            }

            blowSet = new BlowSet(testSpec.Stage, testSpec.NumRows, testSpec.TenorWeight, testSpec.ErrorType, true);

            // No need for an audio suffix in a Gap test (this is used to distinguish A and B in an A/B test)
            blowSet.PopulateBlows(testBlock, testPlace, string.Empty);
            blowSet.CreateRandomSpacing(testSpec.ErrorSize, Constants.Rounding);
            blowSet.SetUnstruck();

            // Set up test spec-dependent elements of the screen object
            // When practicing in a Gap Test, gaps are rounded to the nearest 10ms so that bells will align
            // if zero gap error is selected
            int baseGap = BaseGaps.BaseGap(testSpec.Stage, testSpec.TenorWeight, 10);

            testSpec.BaseGap = baseGap;
            testSpec.GapMin  = 20;

            // If test bell is 1st's place of a handstroke row, need to adjust GapMax to have a higher value
            // because of the handstroke gap
            if (testSpec.NumRows % 2 == 1 && testPlace == 1)
            {
                testSpec.GapMax = Convert.ToInt32(Math.Round(((double)testSpec.BaseGap * 3) / 50)) * 50;
            }
            else
            {
                testSpec.GapMax = Convert.ToInt32(Math.Round(((double)baseGap * 2) / 50)) * 50;
            }
        }
        void Create()
        {
            // Choose whether A or B will have the errors
            Random rand = new Random();

            testSpec.AHasErrors = rand.Next(0, 2) == 0;

            // Create the test block
            Block testBlock = new Block(testSpec.Stage, testSpec.NumRows);

            testBlock.CreateRandomBlock();

            blowSetA = new BlowSet(testSpec.Stage, testSpec.NumRows, testSpec.TenorWeight,
                                   testSpec.ErrorType, testSpec.AHasErrors);
            blowSetA.PopulateBlows(testBlock, testSpec.TestBellLoc, "a");

            // No rounding in an A/B test
            blowSetA.CreateEvenSpacing(1);
            blowSetA.SetUnstruck();

            blowSetB = new BlowSet(testSpec.Stage, testSpec.NumRows, testSpec.TenorWeight,
                                   testSpec.ErrorType, !testSpec.AHasErrors);
            blowSetB.PopulateBlows(testBlock, testSpec.TestBellLoc, "b");

            // No rounding in an A/B test
            blowSetB.CreateEvenSpacing(1);
            blowSetB.SetUnstruck();

            if (testSpec.AHasErrors == true)
            {
                if (testSpec.ErrorType == 1)
                {
                    blowSetA.CreateStrikingError(testSpec.ErrorSize);
                }
                else
                {
                    blowSetA.CreateCompassError(testSpec.ErrorSize);
                }
            }
            else
            {
                if (testSpec.ErrorType == 1)
                {
                    blowSetB.CreateStrikingError(testSpec.ErrorSize);
                }
                else
                {
                    blowSetB.CreateCompassError(testSpec.ErrorSize);
                }
            }

            // Set up test spec-dependent elements of the screen object
            int baseGap = BaseGaps.BaseGap(testSpec.Stage, testSpec.TenorWeight, 1);

            testSpec.BaseGap = baseGap;

            // Set the timing for the animation (when not showing the bells)
            screenA.AnimationDuration = blowSetA.Blows.Last().GapCumulative + 1000;
            screenB.AnimationDuration = blowSetB.Blows.Last().GapCumulative + 1000;
            testSpec.ResultEntered    = false;

            testSpec.ShowGaps = false;
        }