I need help on an implementation of a problem I have encountered. I'm currently coding a Blazor application. My index.razor component has the following code.
#page "/"
#namespace AutoHarp3_Server.Pages
#using ElectronNET.API;
#using ElectronNET.API.Entities;
#inject NavigationManager Navigation
#inject pCharStat pCharStat
<div class="FullColorBackground vh-100">
<div class="d-flex flex-column justify-content-center align-items-center vh-100">
<div class="p-4 bg-light-opactity-light border rounded-lg">
<div class="w-100">
<button class="btn btn-success btn-gradient shadow w-100" #onclick="() => CreateNewCharacter()">Create New Character</button>
</div>
<div class="mt-4 w-100">
<button class="btn btn-success btn-gradient shadow w-100" #onclick="() => OpenCharacter()">Open Character</button>
</div>
</div>
</div>
</div>
#code{
protected override void OnInitialized()
{
pCharStat.electron = new ElectronCls(new pCharacterCls());
pCharStat.electron.BuildMenu();
}
async Task CreateNewCharacter()
{
//User Clicks on button from component I am brought here to make construct a new character.
}
async Task SaveCharacter()
{
//User clicks on button from component I am brought here to save the character to a file
}
async Task OpenCharacter()
{
//User click on button from component I am brought here to open a character file.
}
}
All of this works great from the component. But in Electron there is also the menu options for each of these "NewCharacter", "SaveCharacter", and "OpenCharacter". We can see this from the ElectronCls.cs object shown below.
using AutoHarp3_Server.Data.Classes.CharacterClasses;
using AutoHarp3_Server.Pages;
using ElectronNET.API;
using ElectronNET.API.Entities;
using System;
using Microsoft.AspNetCore.Components;
namespace AutoHarp3_Server.Data.Classes
{
public class ElectronCls
{
public ElectronCls()
{
}
public string FilePath { get; set; } = "";
public void BuildMenu()
{
//Code to build menu
}
public async Task NewCharacter()
{
//Electron Menu will call this function here! How do I gain access to my index.razor component so I can move functionality to that component. I want to do this because my main character object is located there.
}
public async Task ShowOpenDialog(bool CharacterFile = true, bool ImageFile = false)
{
//electron Menu will call this function here! How do I gain access to my index.razor component so I can move functionality to that component. I want to do this because my main character object is located there.
}
public async Task ShowSaveDialog()
{
//electron Menu will call this function here! How do I gain access to my index.razor component so I can move functionality to that component. I want to do this because my main character object is located there.
}
}
}
If the user uses the electron menu I do not see any way from going to the methods I have defined in my ElectronCls.cs object to the index.razor component. I do not see any way to call it. I'm not sure this is possible or if this is the way one should do it in Blazor. The reason I need to find a solution is that since this is an Electron application, the user could use the menu. I should have that option open to them. If there is no way to accomplish this, I suppose I can look into removing the menu altogether from the application.
EDIT: for clarification purposes as requested.
Related
I have a busy view where most of the sections on there work off an ID. I'm looking for a more component way to handle each section so I'm using RenderAction() for each section where they have their own controllers. However I have a search section/"component" and when they put in a new Id and submit on that section/"component", I need a way for that to communicate to all the other RenderActions() that new Id so they can do their thing (query DB to get more info specific to that section).
My Search section would be something like:
public class SearchController : Controller
{
[HttpGet]
public ActionResult SearchContract()
{
var vm = new SearchVM();
return PartialView(vm);
}
[HttpPost]
public ActionResult SearchContract(SearchVM Search)
{
return PartialView(Search);
}
}
#using (Html.BeginForm())
{
<div class="row">
<div class="col-md-3">Contract Id</div>
<div class="col-md-6">
#Html.TextBoxFor(m => m.Id, new { #class = "form-control" })
</div>
</div>
<input type="submit" />
}
Let's say ContractHeader is a section/"component" using RenderAction() that hits a different controller and method from the search:
public class ContractController : Controller
{
public ActionResult ContractHeader(int ContractId)
{
// query contracts
return PartialView(vm);
}
}
Again, I'm looking for a more component oriented way with this. Yes it could all be in one controller but that's not what I'm looking for here. I want a more decoupled/compartmentalized approach to these areas on my views but trying to figure out how they can communicate with each other when "events" happen.
I think I have it figured out. Basically on each search "component" (I'm calling components a separate controller and view that you use RenderAction() to get on your main view) the method that gets called when the search button is pressed will return the following code (I subclassed Controller and put tis method in)
public ActionResult RedirectWithQueryString()
{
// get the referrer url without the old query string (which will be the main view)
var uri = new Uri(Request.UrlReferrer.ToString());
var url = Request.UrlReferrer.ToString().Replace(uri.Query, "");
var allQS = System.Web.HttpUtility.ParseQueryString(uri.Query);
var currentQS = System.Web.HttpUtility.ParseQueryString(Request.Url.Query);
var combinedQS = new NameValueCollection();
// update existing values
foreach (var key in allQS.AllKeys)
{
combinedQS.Add(key, allQS[key]);
}
// add new values
foreach (var key in currentQS.AllKeys)
{
if (combinedQS.AllKeys.Contains(key))
combinedQS[key] = currentQS[key];
else
combinedQS.Add(key, currentQS[key]);
}
var finalUrl = url + combinedQS.ToQueryString();
return Redirect(finalUrl);
}
public class ContractSearchController : MyBaseController
{
// GET: ContractSearch
public ActionResult Index(ContractSearchVM model)
{
return PartialView("ContractSearch", model);
}
public ActionResult SearchContracts(ContractSearchVM model)
{
return RedirectWithQueryString();
}
}
public class StopsSearchController : MyBaseController
{
public ActionResult Index(StopsSearchVM model)
{
// query to get some search related reference data like states list for drop down
return PartialView("StopsSearch", model);
}
public ActionResult SearchStops(StopsSearchVM model)
{
return RedirectWithQueryString();
}
}
SearchContracts() and SearchStops() methods are called from their own forms in their own views using HttpGet. In those methods then we are provided with just that forms query string but we also can get the UrlReferrer query string which will have other search forms key/values in it. So RedirectWithQueryString() basically makes sure the final query string has ALL keys required to satisfy the model binding of any search components on the view and will update the given keys with the current value for the current search component that the submit button was on.
So this then causes us to refresh to the current view with all current key/values in query string for all search components which then is calling all the RenderActions() and the values can be passed.
#model FocusFridayComponents.Models.CombinedVM
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "ContractSearch"); }
</div>
</div>
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Index", "StopsSearch"); }
</div>
</div>
</div>
<div class="col-md-6">
<!-- Contract Header -->
<div class="row">
<div class="col-md-12">
#{ Html.RenderAction("Header", "ContractHeader", new { ContractId = Model.ContractSearch.Id }); }
</div>
</div>
<!-- Contract Routes -->
<div class="row">
<div class="col-md-12">
#* #{ Html.RenderAction("Index", "ContractRoutes", new { ContractId = Model.Id }); } *#
</div>
</div>
</div>
In the main view you're working on you just make a VM that combines the search VM's you're using on the view. The model binding will correctly map the query string keys that match the search VM's even when they are inside the combined VM. The catch here would be to make sure the keys/props of each search VM don't share the same names of any kind.
What's interesting is for the RenderAction() for contract and stops search I don't need to pass the model into it. The binding just does this automatically. For ContractHeader and ContractRoutes I am passing in a parameter because the idea is those are separate components and have their own input requirements and those can be named completely separate from any search models you may be using in your view so the binding wouldn't be able to map anything. This is a good thing though as it decouples your actual view components from your search components.
So you would do all of this to get components that are decoupled from each other but can still talk to each other and you can assemble your views and reuse a lot of these components by just gluing the RenderAction() parameters between them. This can help reduce giant monolithic VM's that tend to pop up on complex views you're making.
I have a .NET MVC project with Razor views and I would like to implement search functionality that consists of a dropdown list, a text box and a search button.
The problem is that I would like to implement this search snippet in my _Layout.cshtml file outside of the #RenderBody() call. This means that the search functionality would be accessible on every page (it would be located at the very top right corner).
I'm trying to find out what is a good way of implementing this. I can get it to work but it would involve adding same code (do get dropdown values) to all controllers and actions.
ViewBag.States = new SelectList(db.States, "Id", "Name");
Is there a better way to implement this? It feels very repetitive to do it this way.
You can have a child action method which returns the partial view needed for your header and call this action method in your layout.
Create a view model for the properties needed.
public class AllPageVm
{
public int SelectedItem { set; get; }
public List<SelectListItem> Items { set; get; }
}
Now create an action method in any of your controller. Mark this action method with ChildActionOnly decorator.
public class HomeController : Controller
{
[ChildActionOnly]
public ActionResult HeaderSearch()
{
var vm = new AllPageVm()
{
Items = db.States
.Select(a => new SelectListItem() {Value = a.Id.ToString(),
Text = a.Name})
.ToList()
};
return PartialView(vm);
}
Now in the HeaderSearch.cshtml partial view, you can render whatever markup you want for your search header. Here is a simple example to render the dropdown. You may update this part to include whatever markup you want (Ex : a form tag which has textbox, dropdown and the button etc)
#model AllPageVm
<div>
<label>Select one state</label>
#Html.DropDownListFor(a => a.SelectedItem, Model.Items, "Select")
</div>
Now in your layout, you can call this child action method
<div class="container body-content">
#Html.Action("HeaderSearch", "Home")
#RenderBody()
<hr/>
<footer>
<p>© #DateTime.Now.Year - My ASP.NET Application</p>
</footer>
</div>
Make sure you are calling PartialView method from the HeaderSearch child action method instead of View method. If you call View method, it will recursively call the same method and you will get a StackOverflow exception
I am trying to replace my view components with razor pages but it seems that it's not possible to load a partial razor page because a model is expected to be passed yet it is my understanding that the model for a razor page should be declared in the OnGetAsync method. Here is my code...
Razor Page
#page "{id:int}"
#model _BackgroundModel
<form method="POST">
<div>Name: <input asp-for="Description" /></div>
<input type="submit" />
</form>
Razor Page Code-Behind
public class _BackgroundModel : PageModel
{
private readonly IDataClient _dataClient;
public _BackgroundModel(IDataClient dataClient)
{
_dataClient = dataClient;
}
[BindProperty]
public BackgroundDataModel Background { get; set; }
public async Task OnGetAsync(int id)
{
Background = await _dataClient.GetBackground(id);
}
public async Task OnPostAsync()
{
if (ModelState.IsValid)
{
await _dataClient.PostBackground(Background);
}
}
}
Razor View
<div class="tab-pane fade" id="client-background-tab">
<div class="row">
<div class="col-sm-12">
#await Html.PartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 })
</div>
</div>
</div>
Page Load Error
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type '<>f__AnonymousType0`1[System.Int32]',
but this ViewDataDictionary instance requires a model item of type
'WebApp.Pages.Client._BackgroundModel'
In this example (as per MS recommended approach in their docs) the model is set inside the OnGetAsync method which should be run when the page is requested. I have also tried #await Html.RenderPartialAsync("/Pages/Client/_Background.cshtml", new { id = 1 }) but the same error result.
How can I load the razor page into my existing view?
Microsoft confirmed this cannot be achieved and therefore razor pages cannot be used as a replacement for view components.
See the comments of their docs...
MS docs
#RickAndMSFT moderator15 hours ago
#OjM You can redirect to the page, or you can make the core view >code into a partial and call it from both.
Pages are not a replacement for partials or View Components.
View
#using (Html.BeginForm())
{
...form elements
#Html.Action("PartialView")
}
PartialView
if (something) {
<input type="submit" value="Submit" />
} else {
#using (Html.BeginForm())
{
<input type="submit" value="Submit" />
}
Can anybody suggest a way around the above problem?
If the PartialView if statement returns false, I end up with nested forms. I can move the form close bracket within the partial view to avoid nesting the forms and the page renders correctly but this upsets visual studio because it expects to see the close bracket within the view. Does that matter?
Edit:
Based on Chris's comments, is the below modification a better approach? i.e. One form with two submit buttons that call different code within the same action method?
PartialView
if (something) {
<input type="submit" name="btn" value="Submit1" />
} else {
<input type="submit" name="btn" value="Submit2" />
}
Controller
[HttpPost]
public ActionResult Index()
{
if (btn == "Submit1") {
...do a thing
} else {
...do another thing
};
}
<form> tag inside another <form> is not a valid HTML
Refer W3c Spec
Workaround available
http://blog.avirtualhome.com/how-to-create-nested-forms/
I run into the same problem, and came up with a helper that really solves it.
/**
* Ensure consequent calls to Html.BeginForm are ignored. This is particularly useful
* on reusable nested components where both a parent and a child begin a form.
* When nested, the child form shouldn't be printed.
*/
public static class SingleFormExtensions
{
public static IDisposable BeginSingleForm(this HtmlHelper html)
{
return new SingleForm(html);
}
public class SingleForm: IDisposable
{
// The form, if it was needed
private MvcForm _form;
public SingleForm(HtmlHelper html)
{
// single per http request
if (!HttpContext.Current.Items.Contains(typeof(SingleForm).FullName))
{
_form = html.BeginForm();
HttpContext.Current.Items[typeof(SingleForm).FullName] = true; // we need the key added, value is a dummy
}
}
public void Dispose()
{
// close the form if it was opened
if (_form != null)
{
_form.EndForm();
HttpContext.Current.Items.Remove(typeof(SingleForm).FullName);
}
}
}
}
To use it, include the namespace of the extension and do #Html.BeginSingleForm( everywhere you want. Not just inside nested views, but also in the parent.
Points of interest: There is a need to save whether or not the form has opened earlier. We can't have a static or static per thread variable ThreadStatic, as this might be used by many Asp.Net threads. The only single-threaded and per-http-request place to add a variable is the HttpContext.Current.Items dictionary.
There is no limitation to the number of submit buttons. The problem is the nested form elements. To avoid having multiple submit buttons you can either hide them using jquery, or extend this helper to automatically add a submit button at the end.
I am in the process of converting several working asp.net mvc 4 (razor) webpages over to jquery mobile and I am seeing some things I don't understand.
The #Html.DropDownListFor don't always populate.....I have inspected the html using firebug and SOMETIMES when the webpage comes up its dropdown has no values...but it is sporadic...I hate sporadic problems....it is driving me up the wall...
Here is the controller...I have checked when the problem is happening...it is always generating the data and sending it....
public ActionResult SelectWorkCenter()
{
SelectWorkCenterInputModel model = new SelectWorkCenterInputModel();
model.WorkCenterList = wcService.GetWorkCenters().OrderBy(p=>p.WorkCenterName).ToList();
return View(model);
}
Here is the web page with the dropdown that is sporadically working:
#using TBS.Etracs.Web.Main.Areas.WorkCenter.Models
#model SelectWorkCenterInputModel
#{
ViewBag.Title = "Select Work Center";
}
#using (Html.BeginForm("SelectWorkOrder", "Mobile", new {Model.SelectedWorkCenterID }))
{
<div class="divTable">
<div class="divTableRow">
<div class="divTableCell"></div>
<div class="divTableCell">[WorkCenterName (#Vehicles)]</div>
</div>
<div class="divTableRow">
<div class="divTableCell"><label><strong> WorkCenter</strong></label></div>
<div class="divTableCell">
#Html.DropDownListFor(p => p.SelectedWorkCenterID,
new SelectList(Model.WorkCenterList, "WorkCenterID", "WorkCenterName"),
"Select a Work Center",
new { Class = "dropdownstyle",
onchange = "this.form.submit();",
style = "width:220px; height: 40px;font-size: 1.2em;font-weight:bold"
})
</div>
</div>
</div>
<input type="submit" value="Back" onclick="history.back(); return false;" style="margin-right: 20px" />
}
I am in the process of converting the from a divTable over to data-role....and the wierd thing is sometimes this after I convert to data-role things improve and other times it makes the problems worse....
I don't understand why converting to jquery mobile would have any impact on this.....
The other problem I am having is that my buttons don't always fire events back to my controllers now that I have converted over to jquery mobile.... I don't know if this is a related problem or not...
Any ideas what I have missed?
More details.....Now that I know more of what is going on I am seeing that this question has been asked before .... I still have not solved the problem, but it does not seem to be the most likey problem that I have seen in other post .... Here is my model which .... this should be correct (as it was working)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using TBS.Etracs.Web.Main.Models;
namespace TBS.Etracs.Web.Main.Areas.WorkCenter.Models
{
public class SelectWorkCenterInputModel
{
public int SelectedWorkCenterID { get; set; }
public List<AssignedWorkCenter> WorkCenterList { get; set; }
}
}
and
AssignedWorkCenter is an EF generated type with values of
System.Int32 WorkCenterID
System.String WorkCenterName
Ok...I don't understand it , but here is the solution....
The problem was with the specifiction of the Style inside the dropDownListFor....By eliminating the Style line everything works...
#Html.DropDownListFor(p => p.SelectedWorkOrderCode,
new SelectList (Model.WorkOrders, "WorkCenterID", "WorkOrderCodeDesc"),
"Select a Work Order",
new { id = "WorkCenterID",
Class = "dropdownstyle" })