locked
Validation Firing for Required Fields Populated from Code Behind When Form is Submitted RRS feed

  • Question

  • User-939035612 posted

    I have a form for users to submit data, but some fields such as UserID which the model requires are added using code behind as part of the OnPostAsync event. Even though my OnPostAsync should be submitting all the required information the form is throwing an error when I click the submit button and it posts back saying that the UserID field is required. 

    A sample of my code is as follows:

    From Posts.cs model:
    
    [Required]
    	public string Userid
    	{
    		get;
    		set;
    	}
    
    From Index.cshtml.cs
    
    public async Task<IActionResult> OnPostAsync()
    		{
    			if (!ModelState.IsValid)
    			{
    				return Page();
    			}
    			if (User.Identity.IsAuthenticated)
    			{
    				IdentityUser user = await _userManager.GetUserAsync(User);
    				Posts.Userid = await _userManager.GetUserIdAsync(user);
                }
                else
                {
    				return Page();
                }
    			_context.Posts.Add(Posts);
    			
    			await _context.SaveChangesAsync();
    			string targeturl = linkgenerator.postlink(Posts.Postid, Posts.Title);
    			return RedirectToPage(targeturl);
    		}

    There is also a problem when it requires required fields from models that only apply when specific categories are selected. When the user selects a category then a partial page is loaded via jQuery containing the inputs for that specific category. If the user selects Shopping then they get inputs for the Shopping table, Housing for the Housing table, etc. Certain fields are marked as required the models for those tables, but they should only be required if records are being inserted into those specific tables after the record is added to the Posts table. Then they get their data from the partial pages and Posts.Postid for Postid fields in those tables, where a relationship exists between the primary key Postid in the Posts table and a foreign key for Postid in the category specific tables.

    The problem here is that if I select a specific category like "Community" I get errors that fields required for all the other categories are missing. How do I mark something as required only if the corresponding category is selected?

    Saturday, October 24, 2020 1:46 AM

Answers

  • User-939035612 posted

    Here is a workaround that just worked for inserting the date from the date input on the Community partial. Are there any vulnerabilities with this that I am not aware of?

    var newCommunity = new Community();
    						newCommunity.Post = postid;
    						newCommunity.Date = Convert.ToDateTime(HttpContext.Request.Form["Community.Date"]);
    						_context.Community.Add(newCommunity);

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, October 28, 2020 1:05 AM

All replies

  • User-821857111 posted

    Validation takes place before the OnPost(Async) method executes. If you intend to set the UserId property on the server after form submission, it should not be included as a form field in your page.

    Saturday, October 24, 2020 6:13 AM
  • User-939035612 posted

    It is not a form field at all, but at the same time every post must have a UserID. Is there a way to mark the user ID as required in the model without having to have it included with the form before posting? 

    Right now the only workaround I can think of is removing the required annotation and just make every insert that needs it has it programmed anyway.

    Saturday, October 24, 2020 6:29 AM
  • User-821857111 posted

    Please could you post the actual code where the error occurs, and the error message?

    Saturday, October 24, 2020 4:00 PM
  • User-939035612 posted

    I got rid of some errors by removing the required annotations from things that are not populated via the form like UserID, but I still have the problem of category specific required fields being required no matter what category is selected. Those fields must be required whenever a record is inserted into a category specific table, but not when a record is being added to a different category.

    These are some code snippets from the relevant parts of my page:

    Here is an example from the Services model

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace PostAlmostAnything.Models
    {
        public partial class Services
        {
            [Key]
            public int Serviceid { get; set; }
            [Required]
            public int? Post { get; set; }
            [Required(ErrorMessage = "Price Required!")]
            [Range(.00, Double.PositiveInfinity, ErrorMessage = "Must Be Positive Number!")]
            [Display(Name = "Price:")]
            [DataType(DataType.Currency)]
            [Column(TypeName = "decimal(18, 2)")]
            public decimal? Price { get; set; }
            [Required(ErrorMessage = "Service Rate Required!")]
            [Range(1, Double.PositiveInfinity, ErrorMessage = "Service Rate Required!")]
            [Display(Name = "Service Rate:")]
            public int? Servicerate { get; set; }
            [Required(ErrorMessage = "Currency Required!")]
            [Range(1, Double.PositiveInfinity, ErrorMessage = "Currency Required!")]
            [Display(Name = "Currency:")]
            public int? Currency { get; set; }
            [Required(ErrorMessage = "Service Hours Required!")]
            [StringLength(500)]
            [RegularExpression("^(?!.*<[^>]+>).*", ErrorMessage = "No HTML tags allowed!")]
            [Display(Name = "Service Hours:")]
            public string Hours { get; set; }
    
            public virtual Currency CurrencyNavigation { get; set; }
            public virtual Posts PostNavigation { get; set; }
            public virtual Servicerate ServicerateNavigation { get; set; }
        }
    }

    Then the form used to create a new post loads partials like this when a category is selected:

     <div class="form-group row">
                <label class="col-sm-2 col-form-label" asp-for="Posts.Category"></label>
                <div class="col-sm-7">
                    <select asp-for="Posts.Category" class="form-control" asp-items="ViewBag.ListofCategories">
                        <option value="0">Select Category</option>
                    </select>
                </div>
                <span class="text-danger col-sm-3 col-form-label" asp-validation-for="Posts.Category"></span>
            </div>
    
    <div id="categorypartial" class="form-group">
    
            </div>
    
    
    $('#Posts_Category').change(function () {
                    var url = '@Url.Content("~/api/")' + "Categories/Getsubcategory/";
                    var ddlsource = "#Posts_Category";
                    $.getJSON(url + $(ddlsource).val(), function (data) {
                        var items = '';
                        $("#Posts_Subcategory").empty();
                        $.each(data, function (i, subcategory) {
                            items += "<option value='" + subcategory.value + "'>" + subcategory.text + "</option>";
                        });
                        $('#Posts_Subcategory').html(items);
                    });
                    $('#categorypartial').load('/post/' + $(ddlsource).val() + '?handler=CategoryPartial', function () {
                        // Remove previous validator
                        $("#newpost").removeData("validator");
                        $("#newpost").removeData("unobtrusiveValidator");
                        $.validator.unobtrusive.parse("#newpost");
                    });
                });

    Then in the Index.cshtml.cs file I have this

    [BindProperty]
    		public Posts Posts
    		{
    			get;
    			set;
    		}
    		
    		[BindProperty]
    		public Community Community
            {
    			get;
    			set;
            }
    		[BindProperty]
    		public Housing Housing
            {
    			get;
    			set;
            }
    		[BindProperty]
    		public Jobs Jobs
            {
    			get;
    			set;
            }
    		[BindProperty]
    		public Personals Personals 
    		{
    			get;
    			set;
    		}
    		[BindProperty]
    		public Rantrave Rantrave
            {
    			get;
    			set;
            }
    		[BindProperty]
    		public Services Services
            {
    			get;
    			set;
            }
    		[BindProperty]
    		public Shopping Shopping
            {
    			get;
    			set;
            }
    		public InputModel Input
    		{
    			get;
    			set;
    		}
    		public IActionResult OnGet()
    		{
    			ViewData["ListofCategories"] = new SelectList(_context.Categories.Where(c => c.Site == GlobalStatic.SITENUMBER()), "Categoryid", "Categoryname");
    			return Page();
    		}
    		public PartialViewResult OnGetCategoryPartial(int Categoryid)
    		{
    			string partialname = String.Empty;
    			switch (Categoryid)
    				{
    					case 1:
    						partialname = "_CreateCommunity";
    						break;
    					case 2:
    						partialname = "_CreatePersonals";
    						break;
    					case 3:
    						partialname = "_CreateHousing";
    						break;
    					case 4:
    						partialname = "_CreateShopping";
    						break;
    					case 5:
    						partialname = "_CreateServices";
    						break;
    					case 6:
    						partialname = "_CreateJobs";
    						break;
    					case 8:
    						partialname = "_CreateRantrave";
    						break;
    				}
    			return Partial(partialname);
    		}

    Here are the parts of the post event relevant to Posts and categories

    public async Task<IActionResult> OnPostAsync()
    		{
    			if (!ModelState.IsValid)
    			{
    				return Page();
    			}
    			// Set Parameters That Don't Require User Input
    			Posts.Adminban = false;
    			Posts.Active = true;
    			Posts.Dateposted = DateTime.Now;
    			Posts.Site = GlobalStatic.SITENUMBER();
    			Posts.Ipaddress = _accessor.HttpContext.Connection.RemoteIpAddress.ToString();
    			int? sc = Convert.ToInt32(HttpContext.Request.Form["Subcategoryid"]);
    			int? sc2 = Convert.ToInt32(HttpContext.Request.Form["Subcategory2id"]);
    			Posts.Subcategory = sc;
    			Posts.Subcategory2 = sc2;
    			if (User.Identity.IsAuthenticated)
    			{
    				IdentityUser user = await _userManager.GetUserAsync(User);
    				Posts.Userid = await _userManager.GetUserIdAsync(user);
                }
                else
                {
    				return Page();
                }
    			_context.Posts.Add(Posts);
    			if (Posts.Category != null)
                {
    				int? catid = Posts.Category;
    				switch (catid)
                    {
    					case 1:
    						Community.Post = Posts.Postid;
    						_context.Community.Add(Community);
    						break;
    					case 2:
    						Personals.Post = Posts.Postid;
    						_context.Personals.Add(Personals);
    						break;
    					case 3:
    						Housing.Post = Posts.Postid;
    						_context.Housing.Add(Housing);
    						break;
    					case 4:
    						Shopping.Post = Posts.Postid;
    						_context.Shopping.Add(Shopping);
    						break;
    					case 5:
    						Services.Post = Posts.Postid;
    						_context.Services.Add(Services);
    						break;
    					case 6:
    						Jobs.Post = Posts.Postid;
    						_context.Jobs.Add(Jobs);
    						break;
    					case 8:
    						Rantrave.Post = Posts.Postid;
    						Rantrave.Nameurl = linkgenerator.rrtagurl(Rantrave.Name);
    						_context.Rantrave.Add(Rantrave);
    						break;
                    }
    
                }
    			await _context.SaveChangesAsync();
    			string targeturl = linkgenerator.postlink(Posts.Postid, Posts.Title);
    			return RedirectToPage(targeturl);
    		}

    So there it is. For efficiency I did not include all of the model and I limited the code for the dropdown lists to just categories because the partials loaded based on category on the ones with the required fields. 

    The problem seems to be that every partial is being required regardless of which one is actually loaded into the form. The solution to this is probably simple, but I am new to .Net Core and have never built a form this complex before. Even after I fix this issue I still have to add a feature for uploading images.

    UPDATE: I made some changes. I removed the BindProperty for all the category tables in my index.cshtml.cs file and I moved await _context.SaveChangesAsync(); up so that it runs before I need to grab the Postid of the new record. Now I get the following error:

    Unfortunately, When I try to post to Community I get an error saying that

    An unhandled exception occurred while processing the request.

    <div class="titleerror">NullReferenceException: Object reference not set to an instance of an object.</div>

    PostAlmostAnything.Pages.post.IndexModel.OnPostAsync() in Index.cshtml.cs, line 177

    • Stack 
    • Query 
    • Cookies 
    • Headers 
    • Routing

    <div id="stackpage" class="page">

    • NullReferenceException: Object reference not set to an instance of an object.

      • PostAlmostAnything.Pages.post.IndexModel.OnPostAsync() in Index.cshtml.cs

        <button class="expandCollapseButton" data-frameid="frame1">-</button> <div class="source">
        1. await _context.SaveChangesAsync();
        2. if (catid != null)
        3. {
        4. switch (catid)
        5. {
        6. case 1:
        1. Community.Post = Posts.Postid;
        1. _context.Community.Add(Community);
        2. break;
        3. case 2:
        4. Personals.Post = Posts.Postid;
        5. _context.Personals.Add(Personals);
        6. break;

        So, why isn't scoped identity working as described here https://www.entityframeworktutorial.net/faq/how-to-get-id-of-saved-entity-in-entity-framework.aspx

        </div>

    </div>

    According to this https://entityframework.net/retrieve-id-of-inserted-entity what I am doing should work because the record is created the same way right before the Postid is needed and used. I checked the database and confirmed that the new record was created and the new record has a Postid, but Posts.Postid is null according to the page.

    Sunday, October 25, 2020 2:09 AM
  • User-939035612 posted

    Thanks for all the great feedback Microsoft! It really is useful. Now I know that my best option is to export my SQL Sever database to a CSV and insert it into a MySQL database so that I can use PHP to create a site that works or maybe just use Wordpress since at least Wordpress can be trusted to stay basically the same.

    Tuesday, October 27, 2020 2:04 AM
  • User1120430333 posted

    Thanks for all the great feedback Microsoft! It really is useful. Now I know that my best option is to export my SQL Sever database to a CSV and insert it into a MySQL database so that I can use PHP to create a site that works or maybe just use Wordpress since at least Wordpress can be trusted to stay basically the same.

    Of course what you have stated is very dubious. Your real issue IMHO is you don't understand how to effectively use the models, like knowing the difference between a viewmodel and a persistence model and when to use one as opposed to the other. The persistence model should never be used in a view. The job of the viewmodel is to be strong typed to a view and used by the view. 

    https://deviq.com/kinds-of-models/

    https://www.dotnettricks.com/learn/mvc/understanding-viewmodel-in-aspnet-mvc

    Your other problem is that you have too much business and database logic in a page's action method. And before you go off, a Razor page still uses the ASP.NET MVC pipeline.

    Example Razor page solution do things that the links explain along with using SoC in a layered style.

    https://github.com/darnold924/PubCompanyCore3.x

    https://en.wikipedia.org/wiki/Separation_of_concerns#:~:text=In%20computer%20science%2C%20separation%20of,code%20of%20a%20computer%20program.

    https://docs.microsoft.com/en-us/previous-versions/msp-n-p/ee658117(v=pandp.10)

    Tuesday, October 27, 2020 4:10 AM
  • User-821857111 posted

    Thanks for all the great feedback Microsoft!
    None of us here work for Microsoft. We are just regular people who freely provide help when we can.

    Tuesday, October 27, 2020 6:55 AM
  • User-939035612 posted

    It seems that with the BindProperty removed the partials no longer bind to the model. When I try to make a new post in the community category I get an error saying that Community.Date is null even though the partial that I loaded clearly binds an input to Community.Date. Here is the Community partial:

    @model PostAlmostAnything.Pages.post.IndexModel
    @{
    }
    <div class="form-group row">
        <label class="col-sm-2 col-form-label" asp-for="Community.Date"></label>
        <div class="col-sm-5">
            <input class="form-control" asp-for="Community.Date" />
        </div>
        <span class="text-danger col-sm-3 col-form-label" asp-validation-for="Community.Date"></span>
    </div>

    Here is the code behind from an async task for creating a new Community entry:

    var newCommunity = new Community();
    						newCommunity.Post = postid;
    						newCommunity.Date = Community.Date;
    						_context.Community.Add(newCommunity);

    Tuesday, October 27, 2020 11:36 PM
  • User-939035612 posted

    You're right, I don't know the "difference between a viewmodel and a persistence model" because I have never needed to know that before.  I'll check out those links.

    Tuesday, October 27, 2020 11:49 PM
  • User-939035612 posted

    Would it be possible to make the BindProperty annotation conditional on the value of Posts.Category. That way the BindProperty would only apply to objects represented in views corresponding to the selected category?

    Wednesday, October 28, 2020 12:00 AM
  • User-939035612 posted

    Here is a workaround that just worked for inserting the date from the date input on the Community partial. Are there any vulnerabilities with this that I am not aware of?

    var newCommunity = new Community();
    						newCommunity.Post = postid;
    						newCommunity.Date = Convert.ToDateTime(HttpContext.Request.Form["Community.Date"]);
    						_context.Community.Add(newCommunity);

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, October 28, 2020 1:05 AM