How to show value of a navigation property with Entity Framework and MVC? - vb.net

I have the following query and am trying to show the Category name(s) in the index view. I can verify in LinqPad that the category is getting filled for each post.
I have tried #item.PostCategory which shows System.Collections.Generic.HashSet1[be_Categories] while #item.PostCategory.FirstOrDefault.CategoryName throws Object reference not set to an instance of an object. And I can verify that each post has the correct number of categories associated with it via #item.PostCategory.Count.
How exactly do i show the value of be_Categories for each post in the index view?
Public Function SelectAll() As IQueryable(Of PostSummaryDTO)
Implements IPostRepository.SelectAll
db = New BetterBlogContext
Dim posts As IQueryable(Of PostSummaryDTO) = db.be_Posts.OrderByDescending
(Function(x)x.DateCreated).Include(Function(c)
c.be_Categories).Select(Function(s) New PostSummaryDTO With {.PostId = s.PostRowID,
.PostDateCreated = s.DateCreated, .PostText = s.PostContent, .PostGuid = s.PostID,
.PostSummary = s.Description, .PostCategory = s.be_Categories, .PostTitle = s.Title,
.PostIsPublished = s.IsPublished, .PostTag = s.be_PostTag})
Return posts
Entity:
Partial Public Class be_Posts
<Key>
Public Property PostRowID As Integer
Public Property BlogID As Guid
Public Property PostID As Guid
<StringLength(255)>
Public Property Title As String
Public Property Description As String
<AllowHtml> Public Property PostContent As String
Public Property DateCreated As Date?
Public Property DateModified As Date?
<StringLength(50)>
Public Property Author As String
Public Property IsPublished As Boolean?
Public Property IsCommentEnabled As Boolean?
Public Property Raters As Integer?
Public Property Rating As Single?
<StringLength(255)>
Public Property Slug As String
Public Property IsDeleted As Boolean
Public Overridable Property be_PostTag As ICollection(Of be_PostTag)
<ForeignKey("be_Categories")> Public Overridable Property be_Categories As ICollection(Of be_Categories)
Public Property be_PostCategory As ICollection(Of be_PostCategory)
End Class

Ok this seems to work - needed a second for each loop in the index view:
#For Each x In item.PostCategory
#x.CategoryName
Next
The whole code:
<div class="row">
#For Each item In Model
#<div class="col-xs-6 col-sm-6 col-lg-6 col-md-6">
<div class="panel panel-default panel-body content">
<b>#item.PostDateCreated.ToShortDateString <span class="pull-right">#item.PostDateCreated.AddHours(3).ToShortTimeString</span></b>
#For Each x In item.PostCategory
#x.CategoryName
Next
<div class="galleryImgWrapper">
<a href="#Url.Action("Details", "Posts", New With {.id = item.Id, .title = BetterBlog.Core.Helpers.SeoHelper.ToSeoUrl(item.PostTitle)}, Nothing)">
#Html.Raw(item.PostSummary.GetImage)
</a>
</div>
<h4>#Html.ActionLink(item.PostTitle, "Details", "Posts",
New With {.id = item.Id, .title = ToSeoUrl(item.PostTitle)}, Nothing)</h4>
#Html.Raw(item.PostSummary.RemoveImgWithRegex)
</div>
</div>
#<div class="clear"></div>
Next
</div>

Related

PageRemote attribute doesn't send __RequestVerificationToken

I'm using .NET 6 and razor pages.
[PageRemote] attribute in POST method doesn't send __requestverificationtoken to server and I get error 400.
This is my ViewModel
public class AddCategory
{
[PageRemote(PageName = "Category", PageHandler = "CheckForTitle",
HttpMethod = "POST",
AdditionalFields = "__RequestVerificationToken",
ErrorMessage = "This title is duplicate")]
public string Title { get; set; } = null!;
}
And this is my handler
public class CategoryModel : PageModel
{
[BindProperty]
public AddCategory Category { get; set; }
public void OnGet()
{
}
public IActionResult OnPostCheckForTitle(AddCategory category)
{
return new JsonResult(category.Title == "a");
}
}
GET method is ok and everything is fine, but in POST method __requestverificationtoken doesn't send to the server and I get error 400.
The property that you are trying to validate is on a nested property. All fields listed in the AdditionalFields property will be prefixed with the nested property name when they are posted, so the request verification token will be posted as Category.__RequestVerificationToken. As a result, the request verification token itself is not found, and request verification fails resulting in the 400 status code.
You should add a separate string property to the PageModel, Title, then apply the PageRemote attribute to that and reference it in the input tag helper via asp-for. Once you are happy that the submission is valid, you can assign the posted Title value to the relevant property in your Category object and process as usual.
public class CategoryModel : PageModel
{
[BindProperty]
public AddCategory Category { get; set; }
[BindProperty, PageRemote(PageName = "Category", PageHandler = "CheckForTitle",
HttpMethod = "POST",
AdditionalFields = "__RequestVerificationToken",
ErrorMessage = "This title is duplicate")]
public string Title { get; set; } = null!;
public IActionResult OnPostCheckForTitle(AddCategory category)
{
return new JsonResult(Title == "a");
}
}
#Mike Brind has been explained very nice. One way you can split the property from model and use asp-for="PropertyName" instead of nested property.
Another way is just override the name by specifying the name attribute like below:
<form method="post">>
//if you use method="post", it will auto generate token
//if you do not use method="post", remember add token like below
#*#Html.AntiForgeryToken()*#
<div class="form-group">
<label asp-for="Category.Title"></label>
//add the name....
<input asp-for="Category.Title" name="Title" class="form-control" />
<span asp-validation-for="Category.Title" class="text-danger"></span>
</div>
</form>
#section Scripts
{
<partial name="_ValidationScriptsPartial" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ajax-unobtrusive/3.2.6/jquery.unobtrusive-ajax.js" integrity="sha256-v2nySZafnswY87um3ymbg7p9f766IQspC5oqaqZVX2c=" crossorigin="anonymous"></script>
}

Get full HTML field name for client side validation in ASP.NET Core

I'm implementing a custom validation attribute. This attribute does not only look at the value of the property it is applied to, but also at the value of another property. The other property is specified by its name.
I need to find a way to get the full id that the input for the other property will have in the final HTML output.
This is a simplified version of my validation attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MyCustomValidationAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
public MyCustomValidationAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var otherProperty = context.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(context.ObjectInstance, null));
// Validation logic...
}
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-mycustomvalidation", errorMessage);
// THIS ROW NEEDS TO BE FIXED
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
}
private void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (!attributes.ContainsKey(key))
{
attributes.Add(key, value);
}
}
}
This demonstrates how it is used in a model class:
public class Report
{
[MyCustomValidation("Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
This is the JavaScript to make sure that the validation is also done on the client side:
$.validator.addMethod('mycustomvalidation',
function (value, element, parameters) {
var otherPropertyValue = $('#' + parameters.otherpropertyname).val();
// Validation logic...
});
$.validator.unobtrusive.adapters.add('mycustomvalidation', ['otherpropertyname'],
function (options) {
options.rules.mycustomvalidation = options.params;
options.messages['mycustomvalidation'] = options.message;
});
My viewmodel for the page/view with the form looks like this:
public MyViewModel
{
public Report MyReport { get; set; }
}
Note that I'm not using Report as my viewmodel, but rather as the type of a property in the viewmodel. This is important since this is the root of my problem...
The code in the view to show the input for Value1 is nothing strange (I'm using Razor Pages):
<div>
<label asp-for="MyReport.Value1"></label>
<input asp-for="MyReport.Value1" />
<span asp-validation-for="MyReport.Value1"></span>
</div>
And the output becomes:
<label for="MyReport_Value1">Value1</label>
<input
type="text"
id="MyReport_Value1"
name="MyReport.Value1"
data-val="true"
data-val-mycustomvalidation="Error..."
data-val-mycustomvalidation-otherpropertyname="Value2"
value=""
>
<span
data-valmsg-for="MyReport.Value1"
data-valmsg-replace="true"
class="text-danger field-validation-valid"
></span>
So the problem is that in the HTML output I need data-val-mycustomvalidation-otherpropertyname to be "MyReport_Value2" instead of just "Value2". Otherwise the validation code won't be able to find the second HTML input (with id MyReport_Value2) and perform the validation.
I figure this must be done in the method AddValidation() in the attribute class, but how do I get the full name that the HTML input will recieve?
I'm guessing there is some way to get this by using the context parameter. I've seen examples of something like "*.TemplateInfo.GetFullHtmlFieldId(PropertyName)" but I can't get it to work.
Any help is appreciated!
You pass Value2 to MyCustomValidationAttribute and set _otherPropertyName with Value2,and use
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
so that html will be
data-val-mycustomvalidation-otherpropertyname="Value2"
You only need to pass Report_Value2 to MyCustomValidationAttribute rather than Value2.
public class Report
{
[MyCustomValidation("Report_Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
So that you will get
data-val-mycustomvalidation-otherpropertyname="Report_Value2"
ValidationContext is binded to instance that belong to validating property i.e Model. Hence locating reference of ViewModel looks difficult.
I can provide three different solution you can use which one suits your requirement.
Solution 1:
Using ValidationContext you can able to get Name of the class where Property belong to. This solution will work only if ViewModel Property Name must be same as Model Class Name.
e.g. if Model Class is Student then property name must be Student. If property name is Student1 it wont work.
Solution 2 & 3 will work even if Class name and property name are different.
Model
public class Student
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required]
[Country("Name")]
public string Country { get; set; }
}
ViewModel
public class StudentViewModel
{
public Student Student {get;set;} //Solution 1 wil not work for Student1
}
ValidationAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CountryAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
private string _clientPropertyName;
public CountryAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(validationContext.ObjectInstance, null));
_clientPropertyName = otherProperty.DeclaringType.Name +"_"+ otherProperty.Name;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", _clientPropertyName);
}
}
Solution 2:
Using ClientModelValidationContext you can able to get ViewModel reference that is passed from the controller to view. By using reflection we can get the name of the property i.e Model.
To work with solution you need to pass empty ViewModel reference from controller.
Controller
public IActionResult New()
{
StudentViewModel studentViewModel = new StudentViewModel();
return View(studentViewModel);
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var viewContext = context.ActionContext as ViewContext;
if (viewContext?.ViewData.Model is StudentViewModel)
{
var model = (StudentViewModel)viewContext?.ViewData.Model;
var instanceName = model.GetType().GetProperties()[0].Name;
otherClientPropName = instanceName + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
Solution 3:
Using context.Attributes["id"] you can able to get current property id value as string . By using string manipulation you can get prefix then you can merge with other property name.
This solution doesn't require empty ViewModel reference from controller.
Controller
public IActionResult New()
{
return View();
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var id = context.Attributes["id"];
var idPrefix = id.Split("_");
if (idPrefix.Length > 1)
{
otherClientPropName = idPrefix[0] + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
HTML Output
<input class="form-control" type="text" data-val="true" data-val-required="Please enter name" id="Student_Name" name="Student.Name" value="">
<input class="form-control input-validation-error" type="text" data-val="true" data-val-mycustomvalidation-otherpropertyname="Student_Name" data-val-required="The Country field is required." id="Student_Country" name="Student.Country" value="">
This is a method that also works when there are fields rendered that are deeper children of the model.
//Build the client id of the property name.
var dependentClientId = dependentPropertyName;
var clientId = context.Attributes["id"];
var clientIdArr = clientId.Split("_");
if (clientIdArr.Length > 1)
{
//Replace the last value of the array with the dependent property name.
clientIdArr[clientIdArr.Length - 1] = dependentPropertyName;
dependentClientId = string.Join("_", clientIdArr);
}
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", dependentClientId );

Razor Pages CheckBox throwing error in Edit Page

I need some help in inserting checkbox value to mssql database and retrieving the same in edit page.
This is model class
public class RequestForm{
[Key]
public int ID { get; set; }
public string OtherCompetitorsChecked { get; set; }
public string OtherCompetitorName { get; set; }
}
This is my RequestForm.cshtml file
<div class="tr">
<div class="td">
<input id="ChkOthers" style="margin-left:40px;" asp-for="RequestForm.OtherCompetitorsChecked" type="checkbox" value="Others" /> Others
</div>
<div class="td">
<input id="CompetitorsOthersName" title="Please Fill In Other Competitor Name" asp-for="RequestForm.OtherCompetitorName" type="text" class="form-control-slp" required disabled style="width:50%" />
</div>
</div>
When checking im inserting the checkbox value into database thats why i used string datatype in model class.Im able to insert the data to the database,when im fetching the data its showing error like below
InvalidOperationException: Unexpected expression result value 'Others' for asp-for. 'Others' cannot be parsed as a 'System.Boolean'.
is there any way to fix this?
InvalidOperationException: Unexpected expression result value 'Others'
for asp-for. 'Others' cannot be parsed as a 'System.Boolean'.
public string OtherCompetitorsChecked { get; set; }
This issue relates the OtherCompetitorsChecked data type. The Input Tag Helper sets the HTML type attribute based on the .NET type. Form the following list, we can see that, for the Input checkbox, the .Net type should be Bool type.
So, to solve the above issue, change the OtherCompetitorsChecked's data type to the bool type:
public class RequestForm
{
[Key]
public int ID { get; set; }
public bool OtherCompetitorsChecked { get; set; }
public string OtherCompetitorName { get; set; }
}
Then, in the Get or Post method, when you set its value, it should be true or false.
public void OnGet()
{
RequestForm = new RequestForm() {
ID = 1001,
OtherCompetitorName = "AA",
OtherCompetitorsChecked = true
};
}
Besides, in your application, might be you want to display a list of selected items using checkbox, and want to use a string type to store the selected value. If that is the case, you could try to use the html <input type="checkbox" name ="selectedCourses"> element to display the items (without using Tag helper) or use a <select> tag, then, in the Post method, get the selected option's value via the html element's name property.
Code like this (you could change the checkbox value to the model data, model detail information, refer this tutorial):
<input type="checkbox" name="selectedCompetitorName" value="Others" />
the Post method:
public void OnPost(string selectedCompetitorName)
{
if (ModelState.IsValid)
{
var data = RequestForm;
}
}
<input id="CompetitorsOthersName" title="Please Fill In Other Competitor Name" asp-for="RequestForm.OtherCompetitorName" type="text"
class="form-control-slp" required disabled style="width:50%" />
At the end, according to the above code, I assume you want to make the CompetitorsOthersName text box readonly, if that is the case, try to remove the disabled attribute, and add the readonly attribute. Because, if using the disabled attribute, after submitting the form, the submitted model's CompetitorsOthersName property will be null. You can check it.

The value 'some value' is invalid when submitting form with checkboxes in MVC 5

I have a form in an MVC 5 application that has several checkboxes to allow different categories to be selected. However when submitting, I get the message The value 'some value' is invalid. That appears where the error message is for the checkboxes. I have looked and not found a solution.
I have an editor template:
#Modeltype ienumerable(Of be_Categories)
#For Each x In Model
#<label>#x.CategoryName</label>
#<input type="checkbox" name="PostCategory" value="#x.CategoryID"/>
Next
And in the form I call:
#Html.LabelFor(Function(model) model.PostCategory, htmlAttributes:=
New With {.class = "control-label col-md-2"})
<div class="col-md-10">
#Html.EditorFor(Function(model) model.PostCategory,
New With {.htmlAttributes = New With {.class = "form-control"}})
#Html.ValidationMessageFor(Function(model) model.PostCategory, "",
New With {.class = "text-danger"})
</div>
And in controller:
Public Function CreateNewPost(ByVal post As be_PostsViewModel) As ActionResult
If ModelState.IsValid Then
Dim p As New be_PostsViewModel
p.PostTitle = post.PostTitle
p.PostDateCreated = post.PostDateCreated
p.IsPublished = post.IsPublished
p.PostGuid = Guid.NewGuid
p.PostSummary = post.PostSummary
p.PostText = post.PostText
p.PostCategory = post.PostCategory
_POSTREPO.Insert(p)
Return RedirectToAction("Index")
End If
Return View(post)
End Function
My Post ViewModel:
Public Class be_PostsViewModel
Public Property Id As Integer
Property Author As String
<DisplayName("Title")> <Required(ErrorMessage:="Your post must have a title")> Public Property PostTitle As String
<DisplayName("My Snarky Text")> Public Property PostSummary As String
<DisplayName("Post")> Public Property PostText As String
<UIHint("DateCreated")> <DisplayName("Date Created")> Property PostDateCreated As DateTime?
<DisplayName("Publish")> Public Property IsPublished As Boolean
Public Property PostGuid As Guid
Public Property BlogId As Guid
<DataType(DataType.MultilineText)> <UIHint("Tags")> <DisplayName("Tags")> Public Property PostTags As ICollection(Of be_PostTag)
<DisplayName("Category")> <UIHint("Categories")> Public Property PostCategory As ICollection(Of be_Categories)
End Class
And the Category Model:
Partial Public Class be_Categories
<Key>
Public Property CategoryRowID As Integer
Public Property BlogID As Guid
Public Property CategoryID As Guid
<StringLength(50)>
Public Property CategoryName As String
<StringLength(200)>
Public Property Description As String
Public Property be_Posts As ICollection(Of be_Posts)
Public Property be_PostCategory As ICollection(Of be_PostCategory)
Public Property ParentID As Guid?
End Class
Can someone tell me where I am going wrong? Also I should mention that I am working with existing database and data so used EF Code First from Database.

ASP.NET MVC 4 - EditorTemplate for nested collections

I have the following model classes (classes simplifies for the purpose of this question):
public class Lesson
{
public Guid Id {get;set;}
public string Name {get;set;}
public List<ExerciseForPupil> Exercises {get;set;}
}
public class ExerciseForPupil
{
public Guid Id {get;set;}
public string Name {get;set;}
public List<ExerciseItemForPupil> ExerciseItems {get;set;}
}
public class ExerciseItemForPupil
{
public Guid Id {get;set;}
public string Content {get;set;}
public string UserValue {get;set;}
}
Now, I want users to be able to fille "UserValue" value for each exercise in the lesson.
Let's say there are 5 exercises for the lesson.
I render these exercises as follows
#Html.EditorFor(x=>x.Lesson.Exercises)
Which renders an EditorTemplate which looks as follows:
#model MyNamespace.ExerciseForPupil
#using (Html.BeginForm("ScoreExercise", "SharedLesson", FormMethod.Post))
{
#Html.Hidden("Id", Model.Id)
#if (Model.ExerciseItems != null)
{
foreach (var exerciseItem in Model.ExerciseItems)
{
#Html.EditorFor(x => exerciseItem, "ExerciseItemForPupil")
}
}
<input type="submit" value="Submit"/>
}
I also have EditorTemplate for "ExerciseItemForPupil":
#model MyNamespace.ExerciseItemForPupil
#Html.HiddenFor(model => model.Id)
#Html.TextBoxFor(model => model.UserValue)
Problem:
As can be seen there will be multiple forms on the page. My "ScoreExercise" action is as follows:
public ActionResult ScoreExercise(ExerciseForPupil exercise)
{
//exercise.ExerciseItems is NULL
}
But my nested collection on the second level (ExerciseItems) is null.
What am I doing wrong?
UPDATE
I've changed the code according to #MysterMan advices:
I call editor template for Exercises as follows:
#Html.EditorFor(x => x.Lesson.Exercises)
and inside this EditorTemplate I call Editor Template for my ExerciseItems
#Html.EditorFor(x=>x.ExerciseItems)
this renders the following markup for UserValue property:
<input id="Lesson_Exercises_0__ExerciseItems_1__UserValue" name="Lesson.Exercises[0].ExerciseItems[1].UserValue" type="text" value="">
but it does not work also
Don't use the foreach. EditorTemplates already iterate over collections if you pass it a collection.
#model MyNamespace.ExerciseForPupil
#using (Html.BeginForm("ScoreExercise", "SharedLesson"))
{
#Html.HiddenFor(x => x.Id)
#Html.EditorFor(x => x.ExerciseItemsForPupil)
<input type="submit" value="Submit"/>
}
A few things to note. You don't have to pass the template name, as the template name is already the same name as the item type. You don't have to use the Post formmethod, as that's the default. There is no name of the property for List so I just assumed it was the plural.
Your last class is also illegal, you would not specify it as a List like that.