I find myself pasting this code over and over on many views that deal with forms.
Is there a simple approach to refactor the following markup from a view in MVC2?
The only changing part is the route for the cancel link (LocalizedSaveButton and LocalizedCancelLink are helper methods I created).
I tried extracting it to a PartialView but I lose the BeginForm functionality. Any suggestions?
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<div class="span-14">
<% Html.EnableClientValidation(); %>
<%using (Html.BeginForm())
{%>
<fieldset>
<legend>Edit Profile</legend>
<%=Html.AntiForgeryToken() %>
<%=Html.EditorForModel() %>
<div class="span-6 prepend-3">
<%=Html.LocalizedSaveButton() %>
<%=Html.LocalizedCancelLink(RoutingHelper.HomeDefault()) %>
</div>
</fieldset>
<%}%>
</div>
You could wrap your code in an Html-Extension like the BeginForm. From your code call the BeginForm on the correct place.
You should return an object that implements IDisposable. In the dispose you call the Dispose of the stored result to BeginForm.
You end up with:
<% using (Html.MyBeginForm()) { %>
<%=Html.LocalizedSaveButton() %>
<%=Html.LocalizedCancelLink(RoutingHelper.HomeDefault()) %>
<% } %>
The trick is not to return a string or MvcHtmlString, but directly write the output using:
htmlHelper.ViewContext.Writer.Write(....);
It would do something like:
public class MyForm : IDisposable {
private MvcForm _form;
private ViewContext _ctx;
public MyForm(HtmlHelper html, /* other params */) {
_form = html.BeginForm();
_ctx = html.ViewContext;
}
public Dispose() {
_form.Dispose();
_ctx.Writer.Write("html part 3 => closing tags");
}
}
and the extension:
public static MyForm MyBeginForm(this HtmlHelper html /* other params */) {
html.ViewContext.Writer.Write("html part 1");
var result = new MyForm(html);
html.ViewContext.Writer.Write("html part 2");
return result;
}
Disclaimer: This is untested code.
Here is what I came up with:
It mostly works but I haven't been able to get the Html.EnableClientValidation() to cooperate with the rendered form.
namespace System.Web.Mvc
{
public class MyForm : IDisposable
{
private bool _disposed;
private readonly HttpResponseBase _httpResponse;
public MyForm(HttpResponseBase httpResponse)
{
if (httpResponse == null)
{
throw new ArgumentNullException("httpResponse");
}
_httpResponse = httpResponse;
}
public void Dispose()
{
Dispose(true /* disposing */);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
_httpResponse.Write("</form>");
}
}
public void EndForm()
{
Dispose(true);
}
}
}
public static class MyFormExtensions
{
public static MyForm FormHelper(
this HtmlHelper htmlHelper,
string formAction,
FormMethod method,
IDictionary<string, object> htmlAttributes,
string formLegendTitle)
{
TagBuilder tagBuilder = new TagBuilder("form");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("action", formAction);
tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true);
HttpResponseBase httpResponse = htmlHelper.ViewContext.HttpContext.Response;
httpResponse.Write(tagBuilder.ToString(TagRenderMode.StartTag));
return new MyForm(httpResponse);
}
public static MyForm MyBeginForm(this HtmlHelper html, string formLegendTitle) {
string formAction = html.ViewContext.HttpContext.Request.RawUrl;
var result = FormHelper(html,formAction, FormMethod.Post, null, formLegendTitle );
html.ViewContext.Writer.Write("<fieldset>");
html.ViewContext.Writer.Write(String.Format("<legend>{0}</legend>", formLegendTitle));
html.ViewContext.Writer.Write("<div class=\"span-14\">");
html.ViewContext.Writer.Write(html.AntiForgeryToken());
html.ViewContext.Writer.Write(html.EditorForModel());
html.ViewContext.Writer.Write("<div class=\"span-6 prepend-3 buttons\">");
html.ViewContext.Writer.Write(html.LocalizedSaveButton());
html.ViewContext.Writer.Write("</div>");
html.ViewContext.Writer.Write("</div>");
html.ViewContext.Writer.Write("</fieldset>");
return result;
}
public static void EndForm(this HtmlHelper htmlHelper)
{
HttpResponseBase httpResponse = htmlHelper.ViewContext.HttpContext.Response;
httpResponse.Write("</form>");
}
}
Related
I am making an configurable form—an admin selects which inputs are displayed. The inputs are wrapped by fieldsets.
I want to make a HtmlHelper to generate fieldset only if it has content—at least one input field to prevent such situations
<fieldset id="Name">
<legend>Name</legend>
<input type="text" placeholer="Forename"></input>
<input type="text" placeholer="Surname"></input>
</fieldset>
<fieldset id="Address">
<legend>Address</legend>
</fieldset>
that we have empty fieldset like that with address.
I've made a FieldSetHelper
public class FieldSetHelper: IDisposable
{
private readonly HtmlHelper _htmlHelper;
private readonly string _fieldSetId;
private readonly string _legendId;
private readonly string _legendText;
public FieldSetHelper(HtmlHelper htmlHelper, string fieldSetId, string legendId, string legendText)
{
_htmlHelper = htmlHelper;
_fieldSetId = fieldSetId;
_legendId = legendId;
_legendText = legendText;
_htmlHelper.ViewContext.Writer = new StringWriter();
OpenFieldSet();
AddLegend();
}
private void OpenFieldSet()
{
string id = string.IsNullOrWhiteSpace(_fieldSetId) ? string.Empty : string.Format(" id=\"{0}\"", _fieldSetId);
_htmlHelper.ViewContext.Writer.WriteLine(string.Format("<fieldset" + id + ">"));
}
private void CloseFieldSet()
{
_htmlHelper.ViewContext.Writer.WriteLine(string.Format("</fieldset>"));
}
private void AddLegend()
{
string id = string.IsNullOrWhiteSpace(_legendId) ? string.Empty : string.Format(" id=\"{0}\"", _legendId);
_htmlHelper.ViewContext.Writer.WriteLine("<legend"+id+">");
_htmlHelper.ViewContext.Writer.WriteLine(_legendText);
_htmlHelper.ViewContext.Writer.WriteLine("</legend>");
}
public void Dispose()
{
CloseFieldSet();
}
But I don't know how to not generate it if its content is empty.
Here is part of the View:
using (Html.BeginFieldSet("Address", null, "Address"))
{
#Html.EditorFor(m => m.Address, new {Model.VisibleInputFields})
}
I recently had a similar situation. I was trying to add some "No Items Here" text if the outer element (the one generated from the using element) did not have any items. This is an untested modified version of what I ended up doing but if you are still looking for an answer it should get you close.
public class FieldSetHelper: IDisposable
{
private readonly HtmlHelper _htmlHelper;
private readonly string _fieldSetId;
private readonly string _legendId;
private readonly string _legendText;
public FieldSetHelper(HtmlHelper htmlHelper, string fieldSetId, string legendId, string legendText)
{
_htmlHelper = htmlHelper;
_fieldSetId = fieldSetId;
_legendId = legendId;
_legendText = legendText;
_htmlHelper.ViewContext.Writer = new StringWriter();
OpenFieldSet();
AddLegend();
}
private string BuildOpenFieldSetString(){
string id = string.IsNullOrWhiteSpace(_fieldSetId) ? string.Empty : string.Format(" id=\"{0}\"", _fieldSetId);
return string.Format("<fieldset" + id + ">");
}
private void OpenFieldSet()
{
_htmlHelper.ViewContext.Writer.WriteLine(BuildOpenFieldSetString());
}
private void CloseFieldSet()
{
_htmlHelper.ViewContext.Writer.WriteLine(string.Format("</fieldset>"));
}
private void AddLegend()
{
string id = string.IsNullOrWhiteSpace(_legendId) ? string.Empty : string.Format(" id=\"{0}\"", _legendId);
_htmlHelper.ViewContext.Writer.WriteLine("<legend"+id+">");
_htmlHelper.ViewContext.Writer.WriteLine(_legendText);
_htmlHelper.ViewContext.Writer.WriteLine("</legend>");
}
public void Dispose()
{
var htmlString = _htmlHelper.ViewContext.Writer.ToString();
var index = htmlString.Trim().LastIndexOf(BuildOpenFieldSetString().Trim());
if(html.IndexOf("<input", index) == -1){
_htmlHelper.ViewContext.Writer = new StringWriter();
_htmlHelper.ViewContext.Writer.WriteLine(html.SubString(0,index));
}else{
CloseFieldSet();
}
}
I try to create some Html Helpers which will have an opening tag and closing tag which will include other contents like the Html.BeginForm does.
For example in Razor we can use the Html.BeginForm helper which has the following syntax:
#using (Html.BeginForm())
{
}
This code will include the contents of curly brackets within a and . The only way that I solved opening and closing a tag with contents is by using two html helpers. I define two html helpers:
public static MvcHtmlString StartForm(this System.Web.Mvc.HtmlHelper helper)
{
return new MvcHtmlString("<form>");
}
public static MvcHtmlString EndForm(this System.Web.Mvc.HtmlHelper helper)
{
return new MvcHtmlString("</form>");
}
Then I use the helpers using the following example:
#Html.StartForm()
contents
#Html.EndForm()
But I would like to be able to make one html helper which will have the following format in the view:
#using (Html.MyForm())
{
<text>contents</text>
}
Can someone help me with this problem because I do not know even how to search it.
You can define a class just like the way the MvcForm is implemented. The class below allows you to create a tag which contains other elements.
public class MvcTag : IDisposable
{
private string _tag;
private bool _disposed;
private readonly FormContext _originalFormContext;
private readonly ViewContext _viewContext;
private readonly TextWriter _writer;
public MvcTag(ViewContext viewContext, string tag)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
_viewContext = viewContext;
_writer = viewContext.Writer;
_originalFormContext = viewContext.FormContext;
viewContext.FormContext = new FormContext();
_tag = tag;
Begin(); // opening the tag
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Begin()
{
_writer.Write("<" + _tag + ">");
}
private void End()
{
_writer.Write("</" + _tag + ">");
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
_disposed = true;
End(); // Closing the tag
if (_viewContext != null)
{
_viewContext.OutputClientValidation();
_viewContext.FormContext = _originalFormContext;
}
}
}
public void EndForm()
{
Dispose(true);
}
}
To make use of this MvcTag in the way the MvcForm is used, we have to define an extension
public static class HtmlHelperExtensions
{
public static MvcTag BeginTag(this HtmlHelper htmlHelper, string tag)
{
return new MvcTag(htmlHelper.ViewContext, tag);
}
}
And that's it. Now you can use it as:
#using(Html.BeginTag("div")) #* This creates a <div>, alternatively, you can create any tag with it ("span", "p" etc.) *#
{
<p>Contents</p>
}
base controller:
public class AnaController : Controller
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
ViewBag.AktifKullanici = kullaniciServis.AktifKullanici(KullaniciEposta);
base.OnActionExecuting(filterContext);
}
}
controller that is inherited from above controller:
public class AnasayfaController : AnaController
{
private HaberSitesiDbContext db;
private HaberServis haberServis;
private KullaniciServis kullaniciServis;
public AnasayfaController()
{
this.db = new HaberSitesiDbContext();
this.haberServis = new HaberServis(db);
this.kullaniciServis = new KullaniciServis(db);
}
// !!! following methods called twice !!!
public ActionResult Index()
{
return View();
}
public ActionResult _SolManset()
{
// id si 2 olanlar sol manset haberleri
var haberler = haberServis.PozisyonHaberler(2, 3)
.ToList();
return PartialView(haberler);
}
public ActionResult _Slider()
{
// id si 1 olanlar slider haberleri
var haberler = haberServis.PozisyonHaberler(1, 19)
.ToList();
return PartialView(haberler);
}
public ActionResult _Yazarlar()
{
var yazarlar = haberServis.KoseYazilari(5)
.ToList();
return PartialView(yazarlar);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
layout:
...
<article id="body">
#RenderBody()
</article>
...
Index:
<aside class="aside_small float_left">
#Html.Action("_SolManset", "Anasayfa")
</aside>
<section class="section_middle">
#Html.Action("_Slider", "Anasayfa")
</section>
<aside class="aside_small float_right">
#Html.Action("_Yazarlar", "Anasayfa")
</aside>
I cant find any solution. Any suggestion? There is no extra code, no js code. How can I find where second calling come from?
I need to write the contents of a js file from a convention-driven location (like ~/ClientApp/Controllers/Home/Home.js if loading the view located at ~/Views/Home/Home.cshtml). How do I do this?
Example: if the file ~/Views/Home/Home.cshtml looks like:
<div id="some-partial-view">
<!-- ... -->
</div>
And the file ~/ClientApp/Controllers/Home/Home.Controller.js looks like
function HomeController() {
//some code
}
Then the rendered view returned by the webserver should look like (if using fiddler)
<!--ommitted <html> <body> tags -->
<div id="some-partial-view">
<!-- ... -->
</div>
<script type="text/javascript">
function HomeController() {
//some code
}
</script>
One way is to add an HTML Helper that will do this such as:
<div id="some-partial-view" ng:Controller="HomeController">
<!-- ... -->
</div>
#Html.IncludeController("HomeController")
However, i don't want to repeat this in all partial views.
Any ideas?
You could write a custom view:
public class MyRazorView : RazorView
{
public MyRazorView(ControllerContext controllerContext, string viewPath, string layoutPath, bool runViewStartPages, IEnumerable<string> viewStartFileExtensions, IViewPageActivator viewPageActivator)
: base(controllerContext, viewPath, layoutPath, runViewStartPages, viewStartFileExtensions, viewPageActivator)
{
}
protected override void RenderView(ViewContext viewContext, TextWriter writer, object instance)
{
base.RenderView(viewContext, writer, instance);
var view = (BuildManagerCompiledView)viewContext.View;
var context = viewContext.HttpContext;
var path = context.Server.MapPath(view.ViewPath);
var viewName = Path.GetFileNameWithoutExtension(path);
var controller = viewContext.RouteData.GetRequiredString("controller");
var js = context.Server.MapPath(
string.Format(
"~/ClientApp/Controllers/{0}/{0}.{1}.js",
viewName,
controller
)
);
if (File.Exists(js))
{
writer.WriteLine(
string.Format(
"<script type=\"text/javascript\">{0}</script>",
File.ReadAllText(js)
)
);
}
}
}
and a custom view engine which will return this custom view when a partial view is to be requested:
public class MyRazorViewEngine : RazorViewEngine
{
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
return new MyRazorView(
controllerContext,
partialPath,
null,
false,
base.FileExtensions,
base.ViewPageActivator
);
}
}
which would be registered in Application_Start:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyRazorViewEngine());
}
You probably might need to adjust some of the paths as it was not quite clear in your question where exactly should the js be located but normally you should have enough details in the answer.
it would make more sense to include all you javascript in the outter ring, then include that via index.php or wherever...
Personally I use jQuery and I include each object like so...
//View Objects
(function($){
var views = {
init: function()
{
this.view1();
this.view2();
},
view1 : function()
{
// I am
},
view2 : function()
{
// all yours
}
}
//call/invoke my view objects
$(function(){
//call view object via init()
view.init();
});
})(jQuery);
I have a POJO named "FlashCard" which has a field named "links" which is collection (set) of Link objects. When I submit a FORM to my Action all the POJO fields are populated with values from the form except the collection of "links". I have no idea why this isn't getting populated.
Any advice on how to resolve this problem or how to better troubleshoot it would be much appreciated.
Also, my POJO's collection is a Set. Does it matter (or complicate things) that I'm using a Set and not a List?
I'm including a simplified version of my code below.
Here's my POJO:
public class FlashCard implements java.io.Serializable {
private int flashCardId;
private String question;
private String answer;
private Set<Link> links = new HashSet<Link>(0);
public FlashCard() {
}
public FlashCard(String question, String answer) {
this.question = question;
this.answer = answer;
}
public FlashCard(String question, String answer, Set<Link> links) {
this.question = question;
this.answer = answer;
this.links = links;
}
public int getFlashCardId() {
return this.flashCardId;
}
public void setFlashCardId(int flashCardId) {
this.flashCardId = flashCardId;
}
public String getQuestion() {
return this.question;
}
public void setQuestion(String question) {
this.question = question;
}
public String getAnswer() {
return this.answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
public Set<Link> getLinks() {
return this.links;
}
public void setLinks(Set<Link> links) {
this.links = links;
}
}
Here's the POJO for the Link object:
public class Link implements java.io.Serializable {
private int linkId;
private String url;
private Set<FlashCard> flashcards = new HashSet<FlashCard>(0);
public Link() {
}
public Link(String url) {
this.url = url;
}
public Link(String url, Set<FlashCard> flashcards) {
this.url = url;
this.flashcards = flashcards;
}
public int getLinkId() {
return this.linkId;
}
public void setLinkId(int linkId) {
this.linkId = linkId;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public Set<FlashCard> getFlashcards() {
return this.flashcards;
}
public void setFlashcards(Set<FlashCard> flashcards) {
this.flashcards = flashcards;
}
}
Here's the relevant part of the Action
public class FlashCardAction extends FlashCardsAppBaseAction implements ModelDriven<FlashCard>, Preparable, SessionAware {
static Logger logger = Logger.getLogger(FlashCardAction.class);
FlashCard flashCard = new FlashCard();
Map <String,Object> httpSession;
Session session;
FlashCardPersister fcPersister;
public Map<String, Object> getHttpSession() {
return httpSession;
}
public FlashCard getFlashCard() {
return this.flashCard;
}
public void setFlashCard(FlashCard flashCard) {
this.flashCard = flashCard;
}
public void validate() {
logger.debug("Entering validate()");
if ( flashCard.getQuestion().length() == 0 ){
addFieldError("flashCard.question", getText("error.flashcard.question"));
}
if ( flashCard.getAnswer().length() == 0 ) {
addFieldError("flashCard.answer", getText("error.flashcard.answer"));
}
}
public String saveOrUpdate() {
logger.debug("Entering saveOrUpdate()");
// assume we'll fail
boolean result = false;
// are we creating a New Flash Card or Updating and existing one
// for now, let's assume we are creating a New Flash Card
boolean newFlashCard = true;
// if this is an Update of an existing Flash CArd then we'll have a Flash Card Id other than 0
if (this.flashCard.getFlashCardId() != 0) {
newFlashCard = false;
}
try {
result = fcPersister.saveOrUpdateFlashCard(this.flashCard, session);
// did we save a new FlashCard successfully?
if (result == true && newFlashCard) {
logger.debug("Flash Card created successfully");
this.addActionMessage(getText("actionmessage.flashcard.created"));
}
// did we update an existing Flash Card successfully?
else if (result == true && newFlashCard == false) {
logger.debug("Flash Card updated successfully");
this.addActionMessage(getText("actionmessage.flashcard.updated"));
}
// such a failure
else {
logger.error("unable to create or update FlashCard");
return "error";
}
return "success";
} catch (Exception e) {
logger.error("Exception in createFlashCard():", e);
return "error";
}
}
#Override
public FlashCard getModel() {
return this.flashCard;
}
#Override
public void setSession(Map<String, Object> httpSession) {
this.httpSession = httpSession;
}
#Override
public void prepare() throws Exception {
logger.debug("Entering prepare()");
// get a handle to a Hibernate session
session = getHibernateSession();
// get a handle to the FlashCard persistance utility class
fcPersister = new FlashCardPersister();
}
}
And lastly here's the JSP
<%#page import="com.opensymphony.xwork2.ActionContext"%>
<%#page import="com.opensymphony.xwork2.ActionSupport"%>
<%# page contentType="text/html; charset=UTF-8"%>
<%# taglib prefix="s" uri="/struts-tags"%>
<%# taglib prefix="sjr" uri="/struts-jquery-richtext-tags"%>
<h3><s:text name="label.flashcard.title"/></h3>
<s:actionerror theme="jquery" />
<s:actionmessage theme="jquery"/>
<s:fielderror theme="jquery"/>
<s:form action="saveOrUpdate" method="post">
<s:hidden name="flashCard.flashCardId" />
<s:textfield name="flashCard.question" key="label.flashcard.question" size="66" />
<sjr:tinymce
id="flashCard.answer"
name="flashCard.answer"
key="label.flashcard.answer"
rows="20"
cols="50"
editorTheme="simple"
/>
<s:textfield name="flashCard.links.url" key="label.flashcard.link" size="66" />
<tr>
<td>
<s:submit label="label.flashcard.submit" align="center" theme="simple" />
</td>
<td>
<s:submit key="label.flashcard.cancel" name="redirectAction:list" theme="simple" />
</td>
</tr>
</s:form>
<%((ActionSupport)ActionContext.getContext().getActionInvocation().getAction()).clearErrorsAndMessages();%>
First of all I don't think you can use Set here, because Sets are unordered and you can't get an item from a set by an index or key like List and Map. The only way is to iterate through the set and get the items.
Second assuming you're using a collection other than set, in:
<s:textfield name="flashCard.links.url" key="label.flashcard.link" size="66"/>
You try to set the value of the text field to url field of links which is a collection and doesn't have such a field. So you need to get the specific item from the collection you're editing and pass the value. Like:
<s:textfield name="flashCard.links[0].url" key="label.flashcard.link" size="66"/>
But since you can't get the specific item you are editing I suggest you create a link field in your Action and set the updated link to it. Then you can perform a logic to relace the updated link with obsolete one in you flashcards. Hope this helps.
Since you are using modeldriven and the model is FlashCard, i think the following
<sjr:tinymce
id="flashCard.answer"
name="flashCard.answer"
key="label.flashcard.answer"
rows="20"
cols="50"
editorTheme="simple"/>
should be changed to
<sjr:tinymce
id="flashCard.answer"
name="answer"
key="label.flashcard.answer"
rows="20"
cols="50"
value="answer"
editorTheme="simple"/>
the name field should be given without the prefix flashcard.also you should provide the 'value' attribute in order for it to be pre-populated.