Bound error in Webgrid - asp.net-mvc-4

I want to display list of vehicles from db in webgrid .but when i run code there comes error in view.chstml that "datasource must be bound" though i used correct model name and db name .
Model : Vehicles
public List<Vehicles> Listvehicle(string uid)
{
List<Vehicles> svehicle;
using (var session = MvcApplication.Store.OpenSession())
{
svehicle = (from vehc in session.Query<Vehicles>() where vehc.userid == uid select vehc).ToList();
}
return svehicle;
}
Controller
[HttpGet]
public ActionResult Listvehilce(string uid)
{
string userName = "ksundas7#gmail.com";
Register reg = new Register();
string userId = reg.userid(userName);
Vehicles vehcl = new Vehicles();
List<Vehicles> vehclist = vehcl.Listvehicle(userId);
vehclist.ToList();
// List<Vehicles> svehicle = WebGrid.Listvehicle(userId);
return View();
}
View
#model IEnumerable<MvcMembership.Models.Vehicles>
#{
ViewBag.Title = "Listvehicle";
}
<h2>Listvehilce</h2>
#{
var grid = new WebGrid(source: Model, canPage: true, rowsPerPage: 3, selectionFieldName: "selectedRow");
grid.Pager(WebGridPagerModes.NextPrevious);}
<style type="text/css">
.table { margin: 4px; width: 500px; background-color:#FCFCFC;}
.head { background-color: #C1D4E6; font-weight: bold; color: #FFF; }
.webGrid th, .webGrid td { border: 1px solid #C0C0C0; padding: 5px; }
.altRow { background-color: #E4E9F5; color: #000; }
.gridHead a:hover {text-decoration:underline;}
.description { width:auto}
.selectRow{background-color: #389DF5}
</style>
#grid.GetHtml(tableStyle: "grid",
headerStyle: "head",
alternatingRowStyle: "alt",
selectedRowStyle: "select",
columns: grid.Columns(
grid.Column("vehicleid"),// format: (item) => item.GetSelectLink(item.vehicleid)),
grid.Column("Type", " Type"),
grid.Column("Make", "Make"),
grid.Column("Registration", "Registration"),
grid.Column("Colour", "Colour"),
grid.Column("Model", "Model")
))
Error:
A data source must be bound before this operation can be performed.
Anyone can tell me please what is reason of this error.
thanks in advance
Regards

You did not return the list in view.here is updated code.
[HttpGet]
public ActionResult Listvehilce(string uid)
{
string userName = "ksundas7#gmail.com";
Register reg = new Register();
string userId = reg.userid(userName);
Vehicles vehcl = new Vehicles();
List<Vehicles> vehclist = vehcl.Listvehicle(userId);
vehclist.ToList();
// List<Vehicles> svehicle = WebGrid.Listvehicle(userId);
return View(svehicle);
}

Did you check that the model is not null when it is being bound to the webgrid?
Also, you can change this part in the controller:
List<Vehicles> vehclist = vehcl.Listvehicle(userId);
vehclist.ToList();
with this
IEnumerable vehclist = vehcl.Listvehicle(userId);
List already implements the IEnumerable interface.

Related

How to create custom HtmlHelper for nullable Boolean radio button group in .NET Core

Trying to make a custom MVC control
[Display(Name = "Do you agree?")]
[Required]
public bool? Agree { get; set; }
I have the following solution that I came up with
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Linq.Expressions;
using System.Text;
namespace MyNameSpace
{
public static partial class HtmlHelpers
{
public static IHtmlContent RadioButtonsBooleanFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
var expressionProvider = new ModelExpressionProvider(html.MetadataProvider);
var metadata = expressionProvider.CreateModelExpression(html.ViewData, expression);
var type = metadata.ModelExplorer.ModelType;
if (!(type == typeof(bool) || type == typeof (bool?)))
throw new InvalidCastException("Property must be either bool or bool?.");
var value = (bool?)metadata.Model;
var isYesChecked = value ?? false ? "checked=\"checked\"" : String.Empty;
var isNoChecked = !(value ?? true) ? "checked=\"checked\"" : String.Empty;
var validationMessage = $"The {metadata.Metadata.DisplayName} field is required.";
var output = new StringBuilder();
output.AppendLine("<div class=\"boolRadio\">");
output.AppendLine($"<input data-val=\"true\" data-val-required=\"{validationMessage}\" id =\"{metadata.Name}_yes\" name=\"{metadata.Name}\" type=\"radio\" value=\"True\" {isYesChecked}/><span>Yes</span>");
output.AppendLine($"<input id=\"{metadata.Name}_no\" name=\"{metadata.Name}\" type=\"radio\" value=\"False\" {isNoChecked}/><span>No</span>");
output.AppendLine($"<label for=\"{metadata.Name}_yes\">{metadata.Metadata.DisplayName}</label>");
output.AppendLine("</div>");
output.AppendLine($"<span class=\"field-validation-valid text-danger \" data-valmsg-for=\"{metadata.Name}\" data-valmsg-replace=\"true\"></span>");
return new HtmlString(output.ToString());
}
}
}
css:
.boolRadio{
height: 20px;
}
.boolRadio > span{
margin: 0 10px 0 5px;
}
To use with model property Agree
#Html.RadioButtonsBooleanFor(m=>m.Agree)

#section with RazorEngine

We are trying to generate templates using RazorEngine and Webapi. If i include section in the cshtml code. Exception is thrown. Is there a different way of doing this.
class Program
{
static void Main(string[] args)
{
var config = new TemplateServiceConfiguration
{
Debug = true,
CachingProvider = new DefaultCachingProvider(t => { })
};
var service = RazorEngineService.Create(config);
Engine.Razor = service;
service.AddTemplate("layout", #"<!DOCTYPE html>
<html> <head>
<style>
body
{
font-size: .85em;
font-family: 'Trebuchet MS' , Verdana, Helvetica, Sans-Serif;
color: #232323;
background-color: #fff;
}
header, footer, nav, section
{
display: block;
}
</style>
<title>#Model.Name</title>
</head>
<body>
<div>
#RenderSection('section1',false)
</div>
<div id='content'>
#RenderBody()
</div>
</body>
</html>");
service.AddTemplate("template", #"#{Layout = ""layout"";}
<h3>#Model.Title</h3>
#section section1{
<text>sample content</text>}");
var bookList = new BookList()
{
Name = "Madhavi",
Title = "Top 10 books",
Books = new List<string>
{
"Harry poter1",
"Harry poter2",
"Harry poter3",
"The hunger game1",
"Harry poter1",
"The Hobbit",
"Eat Pray Love",
"A Tale of Two Cities",
"The Lion, the Witch and the Wardrobe",
"Think and Grow Rich"
}
};
service.Compile("template", bookList.GetType());
var result = service.Run("template", bookList.GetType(), bookList);
Console.WriteLine("Result is: {0}", result);
}
}
public class BookList
{
public string Name { get; set; }
public string Title { get; set; }
public List<string> Books { get; set; }
}
Error Shown
Errors while compiling a Template.
Please try the following to solve the situation:
* If the problem is about missing/invalid references or multiple defines either try to load
the missing references manually (in the compiling appdomain!) or
Specify your references manually by providing your own IReferenceResolver implementation.
See https://antaris.github.io/RazorEngine/ReferenceResolver.html for details.
Currently all references have to be available as files!
* If you get 'class' does not contain a definition for 'member':
try another modelType (for example 'null' to make the model dynamic).
NOTE: You CANNOT use typeof(dynamic) to make the model dynamic!
Or try to use static instead of anonymous/dynamic types.
More details about the error:
- error: (49, 24) Too many characters in character literal
Temporary files of the compilation can be found in (please delete the folder): C:\Users\madhavi\AppData\Local\Temp\RazorEngine_oeduzlw2.nyw
The template we tried to compile is:
------------- START -----------
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-size: .85em;
font-family: 'Trebuchet MS', Verdana, Helvetica, Sans-Serif;
color: #232323;
background-color: #fff;
}
header, footer, nav, section {
display: block;
}
</style>
<title>#Model.Name</title>
</head>
<body>
<div>
#RenderSection('section1',false)
</div>
<div id='content'>
#RenderBody()
</div>
</body>
</html>
------------- END -----------
The generated source code is:
------------- START -----------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
</auto-generated>
//------------------------------------------------------------------------------
namespace CompiledRazorTemplates.Dynamic {
using System;
using System.Collections.Generic;
using System.Linq;
public class RazorEngine_090e814295334051a2dbe5f2a700d613 : RazorEngine.Templating.TemplateBase<EmailTemplateGeneration.BookList>
{
public RazorEngine_090e814295334051a2dbe5f2a700d613() {
}
public override void Execute() {
WriteLiteral(#"<!DOCTYPE html>
<html>
<head>
<style>
body {
font-size: .85em;
font-family: 'Trebuchet MS', Verdana, Helvetica, Sans-Serif;
color: #232323;
background-color: #fff;
}
header, footer, nav, section {
display: block;
}
</style>
<title>
");
Write(Model.Name);
WriteLiteral("
</title>\r
</head>\r
<body>
\r <div>
\r");
WriteLiteral(" ");
Write(RenderSection('section1',false));
WriteLiteral("\r
</div>\r <div"); WriteLiteral(" id=\'content\'");
WriteLiteral(">
\r");
WriteLiteral(" ");
Write(RenderBody());
WriteLiteral("\r </div> \r
</body>\r
</html>");
}
}
}
------------- END -----------
List of loaded Assemblies:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
Loaded Assembly: C:\WINDOWS\assembly\GAC_MSIL\Microsoft.VisualStudio.HostingProcess.Utilities\14.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.HostingProcess.Utilities.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Windows.Forms\v4.0_4.0.0.0__b77a5c561934e089\System.Windows.Forms.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Drawing.dll
Loaded Assembly: C:\WINDOWS\assembly\GAC_MSIL\Microsoft.VisualStudio.HostingProcess.Utilities.Sync\14.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.HostingProcess.Utilities.Sync.dll
Loaded Assembly: C:\WINDOWS\assembly\GAC_MSIL\Microsoft.VisualStudio.Debugger.Runtime\14.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.Debugger.Runtime.dll
Loaded Assembly: C:\Users\madhavi\Documents\Visual Studio 2015\TestIntegrationProject\EmailTemplateGeneration\EmailTemplateGeneration\bin\Debug\EmailTemplateGeneration.vshost.exe
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Core\v4.0_4.0.0.0__b77a5c561934e089\System.Core.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Xml.Linq\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.Linq.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Data.DataSetExtensions\v4.0_4.0.0.0__b77a5c561934e089\System.Data.DataSetExtensions.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\Microsoft.CSharp\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.CSharp.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\System.Data\v4.0_4.0.0.0__b77a5c561934e089\System.Data.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Net.Http\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Net.Http.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
Loaded Assembly: C:\Users\madhavi\Documents\Visual Studio 2015\TestIntegrationProject\EmailTemplateGeneration\EmailTemplateGeneration\bin\Debug\EmailTemplateGeneration.exe
Loaded Assembly: C:\Users\madhavi\Documents\Visual Studio 2015\TestIntegrationProject\EmailTemplateGeneration\EmailTemplateGeneration\bin\Debug\RazorEngine.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Configuration\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Configuration.dll
Loaded Assembly: C:\Users\madhavi\Documents\Visual Studio 2015\TestIntegrationProject\EmailTemplateGeneration\EmailTemplateGeneration\bin\Debug\System.Web.Razor.dll
Loaded Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Dynamic\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Dynamic.dll
When searching for the relevant compiler error you will find: Why I'm getting CS1012: "Too many characters in character literal" and CS0019?
After this info you can look at your source again and detect the error:
#RenderSection('section1',false)
Is translated to
Write(RenderSection('section1',false));
You should use " for strings instead of ' in C#.

Namespace interpolation in Less

Is it possible to use namespace interpolation in Less ?
#color: "yellow";
#top-yellow {
.square(#param) {
width: #param;
height: #param;
}
}
.example {
#top-#color; // Use of #top-yellow namespace
.square(10px);
}
Thanks !
Use parametric namespace instead (Notice namespaces and mixins are actually the same thing - i.e. the former is a purely logical abstraction).
#color: yellow;
#top(yellow) {
.square(#param) {
width: #param;
height: #param;
}
}
.example {
#top(#color); // use yellow #top namespace
.square(10px);
}

MVC Ajax with Dynamic Partial View Creation

How can I create dynamic ajax.actionlinks that will call dynamic partial views.
For example:
I have a page that will generate x number of comments
Each comment can be voted up or down (individually)
The number of up votes and down votes are counted into a single integer
Each comment div will have its own ajax.actionlink
Each ajax.actionlink will pass to the controller the ID of the comment
The controller will calculate the total votes and call the partial view to display back into the div with the correct ID.
What have I done so far:
I have been able to create successful ajax.actionlink
That will call a controller and sum the votes
That will call the partial view and display the votes
What is the issue
I don't want to hard code 30-100 different ajax.actionlinks to call 30-100 hard coded partial views.
How can I accomplish this dynamically?
Existing Code:
My ajax.actionlink inside my razor view
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
new { UserPostID = #Model.Id },
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/up_32x32.png\" />"))
My div inside the same razor view to display the returning results from the partial view.
<div id="CountVote" class="postvotes"></div>
My controller
public PartialViewResult VoteUp(int UserPostID)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = 1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = UserPostID;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxx.xxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
And finally my partial view (_TotalVotes.cshtml)
#ViewBag.SumVotes
Now my main view for Viewpost shows the comments in a loop using the viewbag.
foreach (var item in (List<UserComment>)ViewData["Comments"])
{
CommentVote = "cv" + i.ToString();
<div class="postlinewrapper">
<div class="postvotesframe">
<div class="postvotes">
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteUp",
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/up_32x32.png\" />"))
</div>
<div id="#CommentVote" class="#CommentVote">0</div>
<div class="postvotes">
#Html.Raw(Ajax.ActionLink("[replacetext]", "VoteDown",
new AjaxOptions { HttpMethod = "POST", InsertionMode = InsertionMode.Replace, UpdateTargetId = "CountVote" }).ToHtmlString().Replace("[replacetext]",
"<img src=\"/Images/down_32x32.png\" />"))
</div>
</div>
<div class="postleftbar">
#Html.Raw(item.Comment)
</div>
<div class="postrightbar">
<div>
<div class="post_spec">
<div class="post_spec_title">Call Sign: </div>
<div class="post_spec_detail">#item.CallSign</div>
</div>
<div class="post_spec">
<div class="post_spec_title">When: </div>
<div class="post_spec_detail">#item.CommentDate.ToString("dd/MM/yyyy")</div>
</div>
</div>
<br />
<br />
</div>
</div>
i += 1;
}
I have implemented the login to increase or decrease votes up and down:
public PartialViewResult VoteUp(int userPostId)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = 1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = userPostId;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxxx.xxxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
public PartialViewResult VoteDown(int userPostId)
{
try
{
UserVotes vote = new UserVotes();
vote.SubmitedVote = -1;
vote.UserId = Convert.ToInt32(Session["id"]);
vote.UserPostID = userPostId;
ViewBag.SumVotes = postRepository.InsertUserPostVote(vote);
}
catch (Exception e)
{
xxx.xxxx.xxxx().Raise(e);
}
return PartialView("_TotalVotes");
}
Now all this code works for 1 ajax call just fine, but what I need to is to display separate ajax calls for separate divs dynamically.
Try it this way.
Main view
I'm supposing you have a model with a collection property Comments of Comment items
#model MyNamespace.CommentAndOtherStuff
<ul>
#foreach(item in Model.Comments)
{
<li>
<a href="#Url.Action("VoteUp", "VoteControllerName", new { UserPostId = item.Id })"
class="vote-link"
data-id="#item.Id">#item.Votes</a><img src="vote.jpg" />
</li>
}
</ul>
And your controller just returns a class called VoteResult as JSON.
[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
...
var model = new VoteResult
{
UserPostID = UserPostID,
Votes = service.tallyVote(UserPostID)
};
return Json(model);
}
Now hook all of those up with a jQuery event handler and setup an AJAX call
$(document).ready(function() {
$("a.vote-link").on("click", function(event) {
event.preventDefault();
var link = $(this); // the link instance that was clicked
var id = link.attr("data-id");
var url = link.attr("href");
$.ajax({
url: url,
type: "post"
})
.done(function(result) {
// JSON result: { UserPostID: 1, Votes: 5 }
// replace link text
link.html(result.Votes);
});
});
});
But I want a partial view html fagment.
[HttpPost]
public ActionResult VoteUp(int UserPostID)
{
...
var model = new VoteResult
{
UserPostID = UserPostID,
Votes = service.tallyVote(UserPostID)
};
return PartialView("_TotalVotes", model);
}
_TotalVotes partial
#model MyNamespace.VoteResult
#if (Model.Votes < 0)
{
<span class="unpopular">#Model.Votes</span>
}
else
{
<span class="awesome">#Model.Votes</span>
}
And adjust the AJAX callback
.done(function(result) {
link.html(result);
});
Now you could write a helper for the link fragment but it obfuscates things in my opinion (it's a judgement call). All you really need here is the class name and the data-id which your javascript will bind.
Using the Ajax helpers here seems an unnecessary overhead and I suggest you just use jquery methods to update the DOM. Your current code suggests you might be missing some logic to make a comment voting system work, including indicating what action the user may have already performed. For example (and assuming you want it to work similar to SO), if a user has previously up-voted, then clicking on the up-vote link should decrement the vote count by 1, but clicking on the down-vote link should decrement the vote count by 2 (the previous up-vote plus the new down-vote).
Refer to this fiddle for how this might be styled and behave when clicking the vote elements
Your view model for a comment might look like
public enum Vote { "None", "Up", "Down" }
public class CommentVM
{
public int ID { get; set; }
public string Text { get; set; }
public Vote CurrentVote { get; set; }
public int TotalVotes { get; set; }
}
and assuming you have a model that contains a collection of comments
public class PostVM
{
public int ID { get; set; }
public string Text { get; set; }
public IEnumerable<CommentVM> Comments { get; set; }
}
and the associated DisplayTemplate
/Views/Shared/DisplayTemplates/CommentVM.cshtml
#model CommentVM
<div class="comment" data-id="#Model.ID" data-currentvote="#Model.CurrentVote">
<div class="vote">
<div class="voteup" class="#(Model.CurrentVote == Vote.Up ? "current" : null)"></div>
<div class="votecount">#Model.TotalVotes</div>
<div class="votedown" class="#(Model.CurrentVote == Vote.Down ? "current" : null)"></div>
</div>
<div class="commenttext">#Html.DisplayFor(m => m.Text)</div>
</div>
Then in the main view
#model PostVM
.... // display some properties of Post?
#Html.DisplayFor(m => m.Comments)
<script>
var voteUpUrl = '#Url.Action("VoteUp")';
var voteDownUrl = '#Url.Action("VoteDown")';
$('.voteup').click(function() {
var container = $(this).closest('.comment');
var id = container.data('id');
var voteCount = new Number(container.find('.votecount').text());
$.post(voteUpUrl, { id: id }, function(response) {
if (!response) {
// oops, something went wrong - display error message?
return;
}
container.find('.votecount').text(response.voteCount); // update vote count
if (response.voteCount < voteCount) {
// the user previously upvoted and has now removed it
container.find('.voteup').removeClass('current');
} else if (response.voteCount == voteCount + 1) {
// the user had not previously voted on this comment
container.find('.voteup').addClass('current');
} else if (response.voteCount == voteCount + 2) {
// the user previoulsy down voted
container.find('.votedown').removeClass('current');
container.find('.voteup').addClass('current');
}
});
});
$('.votedown').click(function() {
... // similar to above (modify logic in if/elseif blocks)
});
</script>
and the controller method
public JsonResult VoteUp(int id)
{
int voteCount = // your logic to calculate the new total based on the users current vote (if any) for the comment
return Json(new { voteCount = voteCount });
}

Tri state check box for Razor

I need a check box to return nullable bool value. What should I do?
Is there any Tri State Check Box for Razer engine?
I found this question, but the link is not valid.
You could create a TriState class, a custom ModelBinder for this class, and the corresponding Display/Edit templates.
The TriState class should have a property to store the representation of the current state, it doesn't matter how: for example with an enum, with an int that can be -1, 0 or 1, or with a nullable bool bool?
The display template should simply show the state. I.e. it can show different images for the states, and perhaps an associated "label".
The edit template should show the state and have an script that allows to rotate the 3 states when clicked.
For example, the edit/display template could be implemented as a span with a text that would be used as a label for the state, and could have different CSS styles to show the image as a background image for the span. That would make it easy to change the image both in the server and in the client side scripts.
For the edit template, the span should also have:
a hidden field that stores the value in a way that the custom ModelBinder can recover (i.e. format as string to store the value, and parse the string to recover it)
an script that handles the click event for the span and changes the sate, i.e. that updates the value in the hidden field, as well as the style that shows the corresponding background image.
So you'd need:
TriState class
Custom ModelBinder for TriState class. Look here, or here.
Templates for display/edit. Read this blog: Brad Wilson: ASP.NET MVC 2 Templates
Syles to show the different images (define a background image and a padding so that the text doesn't overwrite the image). Besides, the style could change the cursor in the clickable version (editor template), and perhaps change the display on hover, to give the user a hint that the element is clickable.
Client side script to change the state (only for the editor template). For this script I'd recommend to add a "data-" attribute and attach it unobtrusively. See my answer to this question.
You could improve this by implementing three additional properties in the class to store the labels to show for the three states. This values could be added as extra "data-" attributes in the span, so that the client side script can change the label depending on the current state.
I posted a possible solution on dotnet/aspnetcore github and here for .NET core 3.1+
Here is a fiddle.
I implemented this simple component in .NET core 3.1+.
Here is a fiddle.
The css is from bootstrap, needed to display an empty box, a checked one and a box "full" to indicate the states 0,1,-1 (can change these values easily):
<style>
[class^="icon-"], [class*=" icon-"] { display: inline-block; width: 14px; height: 14px; margin-top: 1px; *margin-right: .3em; line-height: 14px; vertical-align: text-top; background-image: url(/img/glyphicons-halflings.png); background-position: 14px 14px; background-repeat: no-repeat; }
.bootstrap-checkbox { display: inline-block; position: relative; width: 13px; height: 13px; border: 1px solid #000; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
.bootstrap-checkbox i { position: absolute; left: -2px; top: -3px; }
.icon-stop { background-position: -312px -72px; }
.icon-check { background-position: -288px 0px; }
</style>
and this is the blazor component:
<div #ref=checkboxElement class="bootstrap-checkbox" #onclick="(e) => OnClick(e)"><i class="#ImgClass"></i></div>#Description
#code {
public ElementReference checkboxElement { get; set; }
[Parameter]
public string Description { get; set; }
[Parameter]
public bool? Checked { get; set; }
public string ImgClass { get; set; }
public int Value { get; set; }
protected override void OnInitialized()
{
Value = Checked switch { null => -1, true => 1, false => 0 };
ImgClass = Value switch { -1 => "icon-stop", 1 => "icon-check", _ => "" };
}
public void OnClick(MouseEventArgs e)
{
switch (ImgClass)
{
case "":
ImgClass = "icon-check";
Value = 1;
break;
case "icon-check":
ImgClass = "icon-stop";
Value = -1;
break;
case "icon-stop":
ImgClass = "";
Value = 0;
break;
}
}
}