I'm getting used to view components in MVC 6, and I asked a similar question a few years ago about partial views. If I build a view component encapsulating a common use-case that requires its own Javascript, where do I put that Javascript? I know that it is dangerous at best to have Javascript in partial views, but it would be a lot simpler to include it in the view component, rather than in the containing view or a separate file that has to be referenced by the containing view.
For example, say I have a view component that has two drop-downs. The selection in the first drop-down determines what items appear in the second drop-down. This is easily handled in Javascript, of course, but where do I put it?
From my experience with ASP.NET 5 View Components, I would say that the best thing to do with them is to keep them isolated and in one place, so they will be easily to manage in long-term projects.
In one of my ASP.NET projects, I've developed View Components structure like this one:
View, Backend code and Model are all in one place, so when you move around the folder, you are sure that you move whole component. Moreover, when you are modyfying them, you have quick access to all of their parts.
It will be convinient to put JavaScript which is highly coupled with a component also in such structure. You can do this by simply creating the file under the component's folder, and then writing a GULP TASK that will copy JS file to wwwroot. From that point, you will be able to link that JavaScript code on component's .cshtml using standard syntax:
<script src="~/Components/yourcomponent.js"></script>
To obtain such a structure in my project, I've extended Razor, to be able to search for my component's CSHTML's in proper place. To do this, I've added this code in Startup.cs:
public partial class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//non relevant code skipped
services.AddMvc().AddRazorOptions(ConfigureRazor);
}
public void ConfigureRazor(RazorViewEngineOptions razor)
{
razor.ViewLocationExpanders.Add(new ViewLocationExpander());
}
}
and the ViewLocationExpander class is:
public class ViewLocationExpander : IViewLocationExpander
{
protected static IEnumerable<string> ExtendedLocations = new[]
{
"/{0}.cshtml"
};
public void PopulateValues(ViewLocationExpanderContext context)
{
//nothing here
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
//extend current view locations
return viewLocations.Concat(ExtendedLocations);
}
}
Then, you invoke component like this (from any .cshtml view):
#await Component.InvokeAsync("NavigationComponent",new NavigationComponentModel())
Related
i currently have the problem, that i want to use a reference of an existing java class in javascript to be able to execute methods from Javascript of the Java Class to use Shepherd (Javascript Tool for guiding users through the web app). Im using Vaadin for Web deployment with java but Shepherd only works through Javascript.
I dont want to execute the JS from the Class with the methods, moreover i want to execute it from an other class, so it looks like this:
User clicks on a Button
Button button = new Button(); button.addClickListener (c -> tourPageController.executeShepherd(UI.getCurrent().getInternals().getTitle());
Java Code to find active Page
public static void executeShepherd(String appTitle) {switch(appTitle) {case "example": UI.getCurrent().getPage().executeJs("window.startTour($0)", <here i want to reference the existing class with the methods);
execute the Shepherd Tour four the explicit page, the user is on (For that, i need to hand over
a reference of the class holding the methods to execute via javascript), its looks something like this in the header:
window.startTour = () => { ... }
Javascript is getting the parameter (the class)
From Javascript im going to use Shepherd, but there are references to Java Methods (these Methods are for the Vaadin Components, to open and close the respectively tabs i want to use in the tour). I use it like this:
beforeShowPromise: function () {
return new Promise(function(resolve) {
classParameter.$server.<method()>;
resolve();
});
},
How can i do this?
What you can do is to set id to the component in Java
component.setId("component-x");
This will mean that the DOM element will be have the id attribute set.
So if you have some bookkeeping of the components in your Java code, you can pass the id as a parameter in that #ClientCallable call, and then search for the component matching that id in your Java code.
If this is about only one view specific component, you do not need anything else than storing reference to the component in a class field (not even the id). But if you indeed have more than one Accordion in your view, the idea is like this.
public class MyView extends Div {
List<Accordion> accordions;
public MyView() {
Accordion accordion = new Accordion();
accordion.setId("accordion-1");
accordions.add(accordion);
...
}
#ClientCallable
public openAccordion(String id, int index) {
accordions.stream().filter(acc ->
acc.getId().equals(id)).findFirst().ifPresent(acc -> acc.open(null));
accordion.open(index);
}
}
For context, see: How can I change Vaadin Components in Java through Javascript
c# code:
public class Person{
public string Name { get; set; }
public age int { get; set; }
}
cshtml code which generates an id of #Person_Name
#Html.DropDownListFor(m => m.Person.Name)
javascript code:
$('#Person_Name').on("change", function () {
//Do something
}
If I change the Person class property name from Name to FullName. The next step is to modify the cshtml code to read as:
#Html.DropDownListFor(m => m.Person.FullName)
I understand you can manually go in and change the Jquery code, but if this change is made and the person making the change is unaware of the jquery, it is going to cause an error. Is there a way to prevent this from happening through some form of notification or logging? Rather than just remembering that the jquery needs to be changed.
You do have the option to use:
#Html.IdFor(x => x.Person.FullName)
Or
#Html.NameFor(x => x.Person.FullName)
That does rely on having small bits of script in pages, but for what id do this is mostly calling functions in .js resources which from my point of view I find better for reuse anyway.
You may still not get alerted to all issues unless you choose to compile your MVC views, you'll probably get a warning if you have the view open. How you set up your project to compile your MVC views will depend on the type of project but a quick Google should help with that one.
I have an application implementing the WPF WebBrowser control. It loads a page containing some JS functions that have to be called from my application, possibly from other threads. Preferrably, I would like to stick to the MVVM pattern and keep the code for parsing the function return in the model. Calling the InvokeScript method on the WebBrowser object should happen on the Dispatcher thread (and thus in the view), since it is a UI element.
The steps I currently take to get this job done is (roughly in pseudo):
- subscribe to the LoadCompleted event of the browser (view)
- set the browser source (model -> viewmodel -> view)
- catch the LoadCompleted event (view -> viewmodel -> model)
- some logic (model)
- invoke script (model -> viewmodel -> view)
- get script result (view -> viewmodel -> model)
- some logic (model)
This results in quite some back-and-forth communication between the model and the view (through the viewmodel). Since I am not that experienced with WPF (or MVVM in that matter), I am wondering whether there is a neater way of accomplishing this task (and by neater I mean: less calls and events between the model, viewmodel and view).
I know this is not exactly the answer to your question, but it might help you even more in the future. Take a look at the component called CefSharp. That's a c# wrapper around Chrome Embedded Framework. It is very MVVM friendly, it is open source, free and it's reasonably easy to develop for. I've recently moved to it from another Chrome wrapper (Awesomium) and very happy with it.
CefSharp allows you to call js functions from the page and it even supports async/await for those calls.
So main point of MVVM is to separate interface (which is platform specific: windows\android\ios\windows phone etc etc) from everything else, so that you could reuse logic (viewmodel\model) on different platforms. So it's clear you cannot call InvokeScript directly from viewmodel - but not because of dispatcher. Dispatcher you can abstract out (for example), since in some cases you need it in your view model. So usually when viewmodel needs to perform operation on view - events (or just delegates) are used:
public class MyViewModel
{
public Func<string, object> InvokeScript { get; set; }
public Func<string, Task<object>> InvokeScriptAsync { get; set; }
public async void Something() {
var result = await InvokeScriptAsync("my script");
// do something
}
}
And in your view:
public class MyView {
private void OnViewModelChanged(MyViewModel vm) {
vm.InvokeScript = text => Dispatcher.Invoke(() => browser.InvokeScript(text));
vm.InvokeScriptAsync = text => browser.InvokeScriptAsync(text); // for example
}
}
I have a web page with a couple of view components, when I click on these components I open a simple editor for it, see image below. If I edit the text and hit enter, I would like to rerender the view component and not the hole page. Is it possible to invoke a view component using javascript to get this behaviour?
With updates, you should now be able to do this with asp.net core. You can return a ViewComponent from an IActionResult, then just call it with jQuery:
[Route("text-editor")]
public IActionResult TextEditor()
{
return ViewComponent("TextEditorViewComponent");
}
Then this can be called from your View with something as simple as this:
function showTextEditor() {
// Find the div to put it in
var container = $("#text-editor-container");
$.get("text-editor", function (data) { container.html(data); });
}
If you need to pass parameters to the ViewComponent you can do so in the IActionResult (maybe they trickle through from the View to there, to the view component, etc).
This is not possible today but you could build a standard proxy for viewcomponents if you'd like though (similarly you could build a proxy to partial views). e.g. you a catchall attribute route on an action.
I am developing a JSF Custom Component, using the information I found on the following book Pro JSF and HTML5 by Apress.
So far, I successfully developed:
the java class to obtain the data to be rendered in the component
the java component class
the java renderer class
the taglib file
an example page to render the taglib
Everything is working fine, the component is successfully rendered.
Now I would like to add javascript events and behaviour to the rendered elements, more specifically, the purpose of my custom component is to render a menu on a web page, and I would like to ad a dropdown effects to the menu entry. I know how to code the whole thing, in JavaScript, what I don't know is:
What is the best practice to add javascript events and behaviour to the element rendered within a custom component?
Where should the JS files be placed? How do I bind the events to the elements? Is it done in the render class, or after, on the web pages?
Thanks, I'm willing to provide more specific information about my code, if required.
Java Component Class
Note: The CosmoMenu class is just a bean. It basically stores a menu tree (a label, an id and a set of children, if any).
package components;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import domain.CosmoMenu;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponentBase;
#FacesComponent(CosmoMenuComponent.COMPONENT_TYPE)
public class CosmoMenuComponent extends UIComponentBase{
/** Component family of {#link CosmoMenuComponent}. */
public static final String COMPONENT_FAMILY = "CosmoMenu";
/** Component type of {#link CosmoMenuComponent}. */
public static final String COMPONENT_TYPE = "CosmoMenu";
#Override
public String getFamily(){
return CosmoMenuComponent.COMPONENT_FAMILY;
}
private CosmoMenu theMenu;
public CosmoMenu getMenu(){
Gson gson = new Gson();
JsonParser jsonParser = new JsonParser();
CosmoMenuAPI myApi = new CosmoMenuAPI();
String strMenu = myApi.getMenu();
JsonElement jEl = jsonParser.parse(strMenu);
theMenu = gson.fromJson(jEl, CosmoMenu.class);
return theMenu;
}
}
If you want your components to be reusable, I encourage you to pack everything in an independent jar. If using Servlet 3.0, you'll be able to easily access the web resources putting them in META-INF/resources. Provide the jar a faces-config.xml and you'll make it JSF annotation scannable:
components
\-(Your cource code)
META-INF
\-faces-config.xml
\-resources (This ends up in docroot)
\-resources
\-js (Here they go your js files)
\-comp (Here your composite components)
\-css (Here your css)
Later on, you'll have to take care of avoiding the specific ids in your composites, as JSF modifies them while rendering. Your best is to pass the current component reference to your JS functions:
<h:inputText styleClass="myInputStyle" onclick="showInputText(this)" />
Just refer to included CSS styles and JS functions.
Last but not least, be careful when including the jar as a web resource, if the file paths remain in conflict with the ones in your web app, they won't be included.
See also:
Exposing resources from jar files in web applications (Tomcat7)
How to reference JSF managed beans which are provided in a JAR file?
How can I know the id of a JSF component so I can use in Javascript
You can include into the facelets wich uses your component an external javascript file by adding the following code:
<script src="#{request.contextPath}/jspath/yourjs.js"></script>
Within the component when you generate the XHTML output give an Id to your menu entries e.g.
<h:outputText id="myid" value="#{bean.value}"/>
and in yourjs.js
$(document).ready(function() {
$("#myid").click(function(){
// dostuff
});
});