skip to Main Content

I have a model for a batter average web app. After entering the player’s name and how many games in a series to average, I wish to collect hits and atbats for each game. There is a class called Game with 2 properties, hits, and atbats (both integers). In the batter model, there is a property that is a List of type Game called series.

I wish to display two text boxes, bound to a local instance of Game.hits and Game.atbats. I want to cycle through the game series length (int) using the same text boxes on the screen until series has the entire list of hits and atbats.

The problem is trying to accomplish this task and set the List series. I would prefer to have the Action AddGame do that, but when I try to repeatedly call the same View and Action, the length value doesn’t decrement. I tried decrementing length in the Action and it would only drop by 1 number and stop. The Hidden’s are necessary, I have found, in order to send their values to the Action.

Currently, the action is empty, so I haven’t included any code for it.

In the Models folder, I have these two classes in one namespace:

public class Batter
{
    
    public double average { get; set; }
    public string ?name { get; set; }
    public int length { get; set; }

    public List<Game> series { get; set; } = new List<Game>();
}

public class Game
{
    public int hits { get; set; }
    public int atbats { get; set; }
}

This is my view:

@model batterup.Models.Batter
@using batterup.Models

@{
    ViewData["PageTitle"] = "Add Game";
}

<div>
    <h1>@ViewData["PageTitle"]</h1>
    <span>For player @Model.name </span>
    <span> with a game series of @Model.length.</span>
    @using (Html.BeginForm("AddGame", "Home", FormMethod.Post))
    {
        var game = new Game();
      
            @Html.HiddenFor( model => model.name );
            @Html.HiddenFor( model => model.length );
            @Html.HiddenFor( model => model.series );
            @Html.TextBox("hits");<br/>
            @Html.TextBox("atbats");<br/>
            <button type="submit">Add</button>
        }
    
    }
  
</div>

This view does display two textboxes and one button correctly.

How do I conditionally render only this each time I wish to add a new Game object to series?

For the record, I am running this on RedHat Linux, Google Chrome, .NET Core, with Visual Studio Code. I am trying to use as few special Helper codes as possible. I’m only using @Html because it works reliably and easily. Would prefer to use HTML <input>.

I have tried repeatedly calling the action and view, but things don’t work out. I have used a for loop around my text box and submission code. That works, but displays length number of textboxes. I don’t want that. I want only the one set of text boxes and one submit button.

I am expecting that when a user presses the Add Game submit button that the Game object is added to the series list and returned back to ( or a new set of ) two text boxes to get the next Game object’s values and the user presses Add Game. The user never sees more than two text boxes and the submit button on the page. When a decrementing ‘length’ value reaches zero, or any other usable counter expires, then we go off and average the values in the list.

I have searched for this for several days and can’t find an exactly similar problem where someone solved it. I know with conditional rendering in Reactjs this would be fairly straightforward, but I am trying to improve my skill with C# and ASP.NET MVC.

UPDATE: SOLVED 07/08/2023

After quite a bit of work and study, I have a possible answer on my own. My thanks to MStodd for patience and help.

When I learned my problem was with the lack of an obvious state mechanism, I began to research how .NET Core provided state. It is done through a service called Session. I practiced with Ben Cull’s example.

This is a minimalist example. I can scale this up to run a batter’s average app without using an array/list.

My Model:

 public class ViewModel
    {
        public int mynumber {get;set;}
    }

My Index Action:

public IActionResult Index()
    {
        HttpContext.Session.SetInt32("AtBats", 0);

        return View(mymodel);
    }

My Totalizing Action:

[HttpPost]
    public IActionResult Totalize(ViewModel model)
    {
        

        if (ModelState.IsValid)
        {
            //You will still get a null warning, but it works
            int atbats = (int)HttpContext.Session.GetInt32("AtBats");
            atbats += model.mynumber;
            HttpContext.Session.SetInt32("AtBats", atbats);
            
            //necessary to view Session value in View (without extra View code)
            ViewBag.AtBats = HttpContext.Session.GetInt32("AtBats");
        }

        return View("Index");
    }

My Index View (all that’s needed for now):

@model app.Models.ViewModel;


@{
    ViewData["Title"] = "Home Page";
    ViewBag.PageTitle = "Testing";
}

<div>
    <h1>@ViewBag.PageTitle</h1>
    
    
    @using (Html.BeginForm("Totalize", "Home", FormMethod.Post))
    {
        @Html.TextBoxFor(model => model.mynumber)
        @Html.ValidationMessageFor(model => model.mynumber)
        <button type="submit">Add</button>
    }
    @if(ViewBag.AtBats != null){
        <p>At Bats: @ViewBag.AtBats</p>
    }

</div>

These Actions and View produce a total in ViewBag.AtBats from whatever number I place in the TextBoxFor and then submit. Repeatedly adding a number in the web page and clicking submit cycles through the Totalize Action and adds up the total that displays in the web page.

I will post the batter’s average app code that descends from this as my answer.

2

Answers


  1. Chosen as BEST ANSWER

    MStodd began heading me in the right direction with a suggestion to look at HttpContext. I recognized this class when I began searching for how .NET Core MVC handles state, specifically state variables. I use these often in React when I utilize useState(). I needed something similar to hold variables across page (View()) changes. I decided not to keep the array of games since I also abandoned it in the React version. The key thing is that state variables are held in HttpContext.Session class using several methods (e.g. .SetString(), .SetInt32()).

    My Model:

    public class ViewModel
        {
            public int hits {get; set;}
            public int atbats {get; set;}
            public bool complete {get; set;} = false;
            public int serieslength {get; set;}
        }
    

    My Index Action:

    public IActionResult Index()
        {
            HttpContext.Session.SetInt32("hits", 0);
            HttpContext.Session.SetInt32("atbats", 0);
            HttpContext.Session.SetInt32("NumGames", 0);
    
            return View();
        }
    

    My AddGame Action (called repeatedly to add hits and at bats from games):

    Note: The (int) cast to HttpContext.Session.GetInt32() avoids an error trying to assign a nullable int to a non-nullable int. VSCode will still give you a warning as does .NET, but because there isn't a null value, it works.

    [HttpPost]
        public IActionResult AddGame(ViewModel model)
        {
            int hits = (int)HttpContext.Session.GetInt32("hits")+model.hits;
            int atbats = (int)HttpContext.Session.GetInt32("atbats")+model.atbats;
            int series = (int)HttpContext.Session.GetInt32("NumGames");
            series++;
            HttpContext.Session.SetInt32("NumGames", series);
            HttpContext.Session.SetInt32("hits", hits);
            HttpContext.Session.SetInt32("atbats", atbats);
            ViewBag.Hits = HttpContext.Session.GetInt32("hits");
            ViewBag.AtBats = HttpContext.Session.GetInt32("atbats");
            ViewBag.Series = HttpContext.Session.GetInt32("NumGames");
    
            if(model.complete){
                return RedirectToAction("Average");
            }else{
                return View("Index");
            }
        }
    

    My Index View (used during Index action and AddGame action):

    @model batterup.Models.ViewModel;
    
    @{
        ViewData["Title"] = "Batter Up";
        ViewBag.PageTitle = "Batter's Average Calculator";
    }
    
    <div>
        <h1>@ViewBag.PageTitle</h1>
         @using (Html.BeginForm("AddGame", "Home", FormMethod.Post))
        {
            @Html.Label("Enter number of hits: ")
            @Html.TextBoxFor(model => model.hits)
            @Html.ValidationMessageFor(model => model.hits)<br/>
            @Html.Label("Enter number of at bats: ")
            @Html.TextBoxFor(model => model.atbats)
            @Html.ValidationMessageFor(model => model.atbats)<br/>
            @Html.Label("Last game (check the box): ")
            @Html.CheckBoxFor(model => model.complete)<br/>
            <button type="submit">Add</button>
        }
        @if(ViewBag.Hits != null){
            <p>Hits: @ViewBag.Hits</p>
        }
        @if(ViewBag.AtBats != null){
            <p>At Bats: @ViewBag.AtBats</p>
        }
        @if(ViewBag.Series != null){
            <p>Series length: @ViewBag.Series</p>
        }
    
    </div>
    
    

    There is a final action and View, but I didn't see it as germane to the problem of cycling through the same action while keeping and modifying variable state.


  2. If that’s the model you’re binding to, you need to make sure all your data is in there. ASP.NET MVC has special list binding syntax, so you could do what I have below.
    You could also change your model so that you’re just binding to the two values from the boxes and have a hidden ID to look up the already persisted player data; I think that’s probably what you were saying but not what your code was.

    Make sure to "View Source" and look at what’s generated, and set a breakpoint in your controller actions and look at the HttpContext.Current.Request.Form, it might clear up some things.

    @using (Html.BeginForm("AddGame", "Home", FormMethod.Post))
        {
            var printedBoxes = false;
    
            @Html.HiddenFor( model => model.name )
            @Html.HiddenFor( model => model.length )
            @for(int i = 0; i < this.Model.series.Count; i++)
            {
                var currentGame = this.Model.series[i];
                if (currentGame.atbats == 0 && currentGame.hits == 0 && !printedBoxes)
                {
                    @Html.TextBoxFor(model => this.Model.series[i].atbats)
                    @Html.TextBoxFor(model => this.Model.series[i].hits)
                    printedBoxes = true;
                }
                else
                {
                    @Html.HiddenFor(model => this.Model.series[i].atbats)
                    <br />
                    @Html.HiddenFor(model => this.Model.series[i].hits)
                }
            }
            <button type="submit">Add</button>
        }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search