For testing purpouses I want to have custom attribute on each, for example, button each page of my app without any manual adding.
Let's say I have page:
#page "/mypage"
<h1>This is page</h1>
<button class="btn btn-primary" #onclick="IncrementCount">Click me</button>
<button class="btn btn-primary" #onclick="IncrementCount">NO, click ME</button>
#code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
and, eventually, after some kind of additional rendering step, I want to see this html in my browser:
<h1>This is page</h1>
<button class="btn btn-primary" my-custom-attribute="button-click-me">Click me</button>
<button class="btn btn-primary" my-custom-attribute="button-no-click-me">NO, click ME</button>
So, the rule for makeing my-custom-attribute content comes from component content itself. It can be described as "component name + dash + text from component to lower case" => button(component name) + - + click-me(text from component to lower case with dashes instead spaces). Rule should be described somewhere in C# code and should "targeting" on list of components(buttons, divs, etc).
Is there any way to achieve that? Maybe there is a way to tweak Blazor rendering process somehow?
I can see no other solution than using a custom generic component. Someone may come up with a better answer. If so, I'll happily delete this one.
Here's a basic custom component to render html elements:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BlazorApp1.Pages
{
public class MyComponentBase : ComponentBase
{
[Parameter] [EditorRequired] public string Tag { get; set; } = "div";
[Parameter] public string MyAttributeValue { get; set; } = string.Empty;
[Parameter] public string? Content { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
private string myAttribute => string.IsNullOrWhiteSpace(Content) ? this.MyAttributeValue : this.Content.Replace(" ", "-");
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, Tag);
builder.AddMultipleAttributes(1, UserAttributes);
if (!string.IsNullOrWhiteSpace(this.myAttribute))
builder.AddAttribute(2, "my-attribute", this.myAttribute);
if (ChildContent is not null)
builder.AddContent(3, ChildContent);
else if (!string.IsNullOrWhiteSpace(Content))
builder.AddMarkupContent(4, Content);
builder.CloseElement();
}
}
}
And a Demo:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<MyComponentBase Tag="button" MyAttribute="Blazor-Component" class="btn btn-primary" #onclick=this.OnClick Content="My Blazor Button" />
<MyComponentBase Tag="div" MyAttributeValue="Survey-Prompt-Div" class="m-2">
<SurveyPrompt Title="How is Blazor working for you?" />
</MyComponentBase>
<div>
#this.message
</div>
#code {
private string message = string.Empty;
private void OnClick()
{
this.message = $"Hello Blazor at {DateTime.Now.ToLongTimeString()} ";
}
}
While this sort of works, it highlights some issues with what you are trying to do.
You can't just get the "content" between the opening and closing tags of an element. The content is a RenderFragment, not a string that you can parse and use for your attribute. Therefore anything like the second block in the example requires a manual setting of the attribute (you may as well define it directly!).
The complexity of the component depends on what type of content you want to apply your attribute to.
Anyway, the component demonstrates the basic principles of how to capture parameters and attributes and use them to construct elements.
Related
I know this is fairly simple, and asked often, but wiring it all together is still new to me.
Each Invoice has an Id:
Model
public class Invoice
{
public int Id { get; set; }
}
I use the viewModel to populate a list of selections for a dropdown, which works great. After returning to a new view, Main, the viewModel is also used to capture the InvoiceId of the selection from the dropdown.
viewModel
public class MainPageViewModel
{
public int InvoiceId { get; set; }
public IEnumerable<Invoice> Invoices { get; set; }
}
Controller
public ActionResult Index()
{
var userId = User.Identity.GetUserId();
var viewModel = new MainPageViewModel
{
Invoices = _context.Invoices.Where(i => i.User.Id == userId).ToList();
};
return View("Main", viewModel);
}
And I want to use razor: #Html.BeginForm() to route to the {controller}/{action}/{id} URL using this cshtml:
View
#model SubSheets.ViewModels.MainPageViewModel
#using (Html.BeginForm(
action: "WorkSummary",
controller: "Invoices",
routeValues: new { id = Model.InvoiceId }))
{
#Html.AntiForgeryToken()
<div class="form-group">
<div class="col-md-10">
#Html.DropDownListFor(
expression: m => m.InvoiceId,
new SelectList(
Model.Invoices,
dataValueField: "Id",
dataTextField: "Name"
)
)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Go To Invoice" class="btn btn-
default" />
</div>
</div>
}
And then simply use convention in the new view, which is building the URL correctly. The problem is the id == 0 no matter which selection is selected.
public ActionResult WorkSummary()
{
return View();
}
As I understand it, the lambda expression in the DropDownListFor determines the data hook in the viewModel object and the dataValueField of the SelectList captures the selection's Id in this case and assigns it to the expression.
I assumed it would complete the assignment and pass the Id to the viewModel and pass that through to the routeValues after submitting the form, however it passes 0 instead of the correct Id.
If I load the new view with the viewModel, I can render the id correctly using razor on the page. E.g.:
#using SubSheets.ViewModels.MainPageViewModel
<p>#Model.InvoiceId.ToString()</p>
What am I missing here?
Edit:
I've tried moving the dropdown outside of the form and making a separate submit button:
<button class="btn btn-default">Button</button>
I feel like I'm trying to force a square peg into a circular opening. Am I going about this incorrectly?
Edit2
Well, I came up with a different solution. Realizing I want to use the Url helper, I used Bootstrap to make each selection clickable which is way sleeker anyway.
<div class="btn-group">
<button
type="button"
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
Select Invoice <span class="caret"></span>
</button>
<ul class="dropdown-menu">
#foreach (var invoice in Model.Invoices)
{
<li><a href="#Url.Action(
"WorkSummary",
"Invoices",
new {id = invoice.Id},
null)">
#invoice.Name
</a></li>
}
</ul>
</div>
then pass the id into the controller, query the database and return the correct entry with the correct URL. It's Ok to spend half a day on something that took me 5 minutes to actually build, right?
Every question on this site, every tutorial which I found is about saving uploaded an image in database using HttpPostedFileBase. Instead of this:
<input type='file'>
I have this in my Registration.cshtml:
<div id="copiedimage">
<img class="default-avatar" src="#Url.Content("~/Content/Images/Account/avatar.png")" alt='default-avatar' />
</div>
This div contains img which will maybe be changed. It will be more clear to understand from the picture:
So, this left circle avatar is actually img from this div. Default image on page load is from the Url.Content, but this image will be changed if user clicks on an image from the slider, or if he chooses his own picture(horse) and clicks on it.
I have UserAvatar model:
public class UserAvatar
{
public int Id { get; set; }
public int UserId { get; set; }
public virtual User User { get; set; }
public byte[] Avatar { get; set; }
}
The view includes the following html for the 'default' avatar and the slider
<div id="copiedimage">
<img class="default-avatar" src="#Url.Content("~/Content/Images/Account/avatar.png")" alt='default-avatar' />
</div>
<div class="slider">
<img class="avatar selected" src="#Url.Content("~/Content/Images/Account/avatar.png")" alt='avatar' />
<img class="avatar" src="#Url.Content("~/Content/Images/Account/avatar_dog.png")" alt='dog' />
<img class="avatar" src="#Url.Content("~/Content/Images/Account/avatar_chihuahua.png")" alt='chihuahua' />
.... // more images
</div>
The script to set assign the selected avatar is
$('.avatar').click(function () {
$('.selected').removeClass('selected');
$(this).addClass('selected');
var selectedAvatar = $('.avatar.selected')[0];
var defaultAvatar = $('#copiedimage img')[0];
defaultAvatar.src = selectedAvatar.src;
defaultAvatar.alt = selectedAvatar.alt;
});
How can I pass this image to Controller in MVC? How can I save that image to the SQL database?
There is no point storing the byte array of the image in the database since its already stored as a file in your folder (although you could do it using the File.ReadAllBytes(fileName); method)
Instead store the path of the file in the database, and you model will now be
public class UserAvatar
{
....
public string AvatarPath { get; set; }
}
In the GET method, pass and instance of UserAvatar, which you can set to the 'default' ("/Content/Images/Account/avatar.png") for a new item, or to the existing value if editing.
Then in the view, include a hidden input for the AvatarPath path property which will be updated if the user selects an image from your slider
<div id="copiedimage">
<img class="default-avatar" src="#Model.AvatarPath" /> // bind to the model
</div>
#Html.HiddenFor(m => m.AvatarPath)
and modify the script to
$('.avatar').click(function () {
$('.selected').removeClass('selected');
$(this).addClass('selected');
var selectedAvatar = $('.avatar.selected')[0];
var defaultAvatar = $('#copiedimage img')[0];
defaultAvatar.src = selectedAvatar.src;
defaultAvatar.alt = selectedAvatar.alt;
$('#AvatarPath').val(selectedAvatar.src); // set the value of the hidden input
});
When you submit your form, say to
[HttpPost]
public ActionResult Create(UserAvatar model)
the model will be correctly bound with the selected image path (or the default if the user did not select an image)
I want to get a value of a HTML text input in my ViewModel :
<div class="InputAddOn"
visible="#bind(vm.resetPasswordEmailDiv)">
<h:input class="InputAddOn-field"
name="resetPasswordEmail" id="resetPasswordEmail"
disabled="#bind(vm.resetPasswordEmailDisabled)"
value="#bind(vm.resetPasswordEmail)"/>
<button class="InputAddOn-item"
onClick="#command('sendResetPasswordEmail')">
Send
</button>
</div>
So when I click on the send button I get the value in my #Command method.
Here even the value="#bind(vm.resetPasswordEmail)" dosen't work
I have copied your code and the binding worked just fine. Below you find an example that uses both the binding and the command parameter:
<div xmlns:h="xhtml" viewModel="#id('vm')#init('somePath.MyViewModel')">
<h:input value="#bind(vm.bindingParam)" />
<button label="Send" onClick="#command('doCommand', commandParam=self.previousSibling.value)" />
</div>
public class MyViewModel
{
String bindingParam;
public String getBindingParam()
{
return bindingParam;
}
public void setBindingParam(String bindingParam)
{
this.bindingParam = bindingParam;
}
#Command
public void doCommand(#BindingParam("commandParam") String commandParam)
{
System.out.println(bindingParam + " " + commandParam);
}
}
When you click the button, it will output null null or myText myText according to the content of the input.
Retrieving the text from the input is done with the same expression language as the bindings. self is the replacement for this, and I used getPreviousSibling but you should be able to use IDs, too.
Ok, so now I'm trying to learn .net core mcv and I'm having a problem mapping data from MySQL back to my form. When I make the form as a single text box with a single button, and the other fields outside the form (but on the same page), the mapping works fine. If I include all the fields within the form, the data is obtained but not displayed on the page. I have even gone so far as to code one of the multiple submit buttons as an update of the data. I use the first text box to get the item from the database, which it does (but does not map to the text-boxes), then in the second text box (which should have the existing data, but is empty) I put the information to update in the database, click on the submit button for that text box, and the database is updated (but the text boxes in the view remain blank).
My model:
using System;
namespace DbTest.Models
{
public class ProductInventory
{
public string Field1 { get; set; }
public string Field2 { get; set; }
public string Field3 { get; set; }
public int Field4 { get; set; }
}
}
my controller:
using System;
using Microsoft.AspNetCore.Mvc;
using MySql.Data.MySqlClient;
using Microsoft.AspNetCore.Authorization;
using DbTest.Models;
namespace DbTest.Controllers
{
public class InventoryController : Controller
{
// [Authorize]
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult ProcessForm(string button, ProductInventory p)
{
IActionResult toDo = null;
if (button == "Button1")
{
toDo = GetItem(p);
}
if (button == "Button2")
{
toDo = UpdateField2(p);
}
if (button == "Button3")
{
toDo = UpdateField3(p);
}
if (button == "Button4")
{
toDo = UpdateField4(p);
}
return toDo;
}
// [HttpPost]
public IActionResult GetItem(ProductInventory p)
{
//CODE SNIP - DATABASE QUERY, IT ALL WORKS, SO WHY BOTHER YOU WITH THE DETAILS?
return View("Index", p);
}
public IActionResult UpdateField2(ProductInventory p)
{
//CODE SNIP - DATABASE UPDATE, ALL WORKS, NOTHING TO SEE HERE
return View("Index", p);
}
}
}
And finally, my view:
#model DbTest.Models.ProductInventory
#{
ViewData["Title"] = "Inventory Page";
}
#using (Html.BeginForm("ProcessForm", "Inventory", FormMethod.Post))
{
<div>
Search Item (Field 1):
#Html.TextBoxFor(model => model.Field1)
<input type="submit" name="button" value="Button1" />
</div>
<div>
Field 2:
#Html.TextBoxFor(model => model.Field2)
<input type="submit" name="button" value="Button2" />
</div>
<div>
Field 3:
#Html.TextBoxFor(model => model.Field3)
<input type="submit" name="button" value="Button3" />
</div>
<div>
Field 4:
#Html.TextBoxFor(model => model.Field4)
<input type="submit" name="button" value="Button4" />
</div>
}
To reiterate, if I close the form after Button1:
#using (Html.BeginForm("ProcessForm", "Inventory", FormMethod.Post))
{
<div>
Search Item (Field 1):
#Html.TextBoxFor(model => model.Field1)
<input type="submit" name="button" value="Button1" />
</div>
}
<div>
Field 2:
//etc.
the mapping works, but only the first field and button of the form work. With the form around all four fields and buttons, the mapping doesn't work, but the coding of the second button DOES update the database on clicking Button2.
Can someone explain what I've done wrong here?
Thanks!
At first, don't use html helpers in ASP.NET Core.They work but it is not best practice. Instead use tag helpers wherever possible. Furthermore, don't use your db models as view models.
Regarding your Index action: You forgot to pass a view model to your view.
In your ProcessForm action you instantiate IActionResult and then assign it with a (action) function. Don't do that. Instead use return RedirectToAction("ActionName");.
In your case I would handle the DB updates inside the ProcessForm action or in a function, which doesn't return IActionResult.
In conclusion, I can only recommend you to read the ASP.NET Core documentation and then ask again if you still don't get it to work. I recommend you to start with reading this.
I've tried implementing the option explained in this article.
public class HttpParamActionAttribute : ActionNameSelectorAttribute
{
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
return true;
if (!actionName.Equals("Action", StringComparison.InvariantCultureIgnoreCase))
return false;
var request = controllerContext.RequestContext.HttpContext.Request;
return request[methodInfo.Name] != null;
}
}
My controller actions:
[HttpParamAction]
[HttpPost]
public virtual ActionResult EditAccouncement(_AccouncementPostViewModel m)
[HttpParamAction]
[HttpPost]
public virtual PartialViewResult DeleteAnnouncement(int id)
My form:
#using (Ajax.BeginForm("Action", ajaxOptions: new AjaxOptions()
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "announcement" + #Model.id
}))
{
//form values omitted
<button type="submit" class="submitbutton" name="edit">Change details</button>
<button type="submit" class="submitbutton" name="delete">Delete</button>
}
However the controller action being called is still just the Action method (which doesn't exist). Am I missing something?
Your problem is caused by mismatch between your action names and the button name attributes.
The value of the name attribute on your buttons needs to match the action names, like so:
<button type="submit" name="EditAccouncement">Change details</button>
<button type="submit" name="DeleteAnnouncement">Delete</button>
Update: I would suggest a different approach all together. This solution seems counter-intuitive to me, and not particularly easy to follow.
You could just as easily use JavaScript (e.g. jquery) to handle the form submits 'manually', by hooking up two different event handlers to your buttons. One event would make a POST to the EditAccouncement (typo!) action and one would make a POST to the DeleteAnnouncement action.
I made a mockup on jsfiddle which demonstrates the code: http://jsfiddle.net/wmWNj/3/
Update 2: fixed typos in jsfiddle