Adding a dropdown list outside or RenderBody call - asp.net-mvc

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

Related

RenderAction() to make decoupled components but how to communicate with other components

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.

MVC: How to get controller to render partial view initiated from the view

In my MVC5 project I want to create a menu in a partial view. This menu is dynamic in the sense that it is built from content in my database. Thus I have a controller taking care of creating my menu and returning the menu model to my partial view:
public PartialViewResult GetMenu()
{
MenuStructuredModel menuStructuredModel = menuBusiness.GetStructuredMenu();
return PartialView("~/Views/Shared/MenuPartial", menuStructuredModel);
}
In my partial view called MenuPartial I want to use razor to iterate over my menu items, like:
#model MyApp.Models.Menu.MenuStructuredModel
<div class="list-group panel">
#foreach (var category in Model.ViewTypes[0].Categories)
{
#category.ShownName
}
</div>
Now the problem is the view in which I insert the partial view. If in the view I simply do:
#Html.Partial("MenuPartial")
It won't call the controller to populate the model with data first. What I want is to let the controller return the partial. But I don't know how to do this from the view. In pseudo code I would like to do something like:
#Html.RenderPartialFromController("/MyController/GetMenu")
Thanks to Stephen Muecke and Erick Cortorreal I got it to work.
This is what the controller should look like:
[ChildActionOnly]
public PartialViewResult GetMenu()
{
MenuStructuredModel menuStructuredModel = menuBusiness.GetStructuredMenu();
return PartialView("~/Views/Shared/MenuPartial", menuStructuredModel);
}
And it may called like:
#Html.Action("GetMenu", "Home")
(Hence GetMenu() is declared in the HomeController in my example).
The controller is now called (and the model is populated) prior to the partial view is rendered.
You should use: #Html.RenderAction or #Html.Action.

Object reference not set to an instance of an object in MVC5

I have an MVC5 app, and in the HomeController, I have an ActionResult defined like this:
public ActionResult BlogRSS()
{
var model = new BlogModel();
string strFeed = "http://webmysite.com/feed";
using (XmlReader reader = XmlReader.Create(strFeed))
{
SyndicationFeed rssData = SyndicationFeed.Load(reader);
model.BlogFeed = rssData;
}
return View(model);
}
Then, for this ActionResult, I created a partial view named BlogRSS, which looks like this:
#model MyApp.Models.BlogModel
#{
if (Model.BlogFeed != null)
{
<ul>
#foreach (var post in Model.BlogFeed.Items.ToList().Take(3))
{
<li><a href='#post.Links.First().Uri' target='_blank'>#post.Title.Text</a></li>
}
</ul>
}
}
And my model is defined simply like this:
public class BlogModel
{
public SyndicationFeed BlogFeed { get; set; }
}
So, the point is that I want to call that partial view in my _Layout.cshtml file, but when the website opens I get the error message specified in the title. I guess it is not calling my BlogRSS method at all. I'm calling it in the _Layout.cshtml like this:
<div class="col-md-4">
Blog
<br />
#Html.Partial("BlogRSS")
</div>
How can I solve the problem, and make sure that the corresponding ActionResult is also called before rendering the View?
The problem is that you're putting a call to a partial view which just renders the view without calling the controller and the model passed to that view is null.
There are couple ways how to fix this:
1) use Action instead of Partial
#Html.Action("BlogRSS", "Blog")
2) Define a base ViewModel which you will pass to the each view and put your feed into it.

Set input column value in code in MVC

I created my first MVC application in ASP.NET today. I have a datetime column "CreatedAt" which should be filled by current date without being visible in the input form. But the generated code has this code:
<div class="editor-field">
#Html.EditorFor(model => model.CreatedAt)
#Html.ValidationMessageFor(model => model.CreatedAt)
</div>
It displays a textbox in input form. I don't want to display it, instead it should be set in code behind. How can I do that?
ASP.NET MVC doesn't have a concept of a 'code-behind'. Quite simply, you send data from your View, and it's processed in your Controller.
So if this is an action that POSTs, then we can send data back to the controller, and even better, we can keep that data 'hidden' from the textbox view.
In your view, you should replace that with the following line:
#Html.HiddenFor(m => m.CreatedAt, DateTime.Now);
Then when the model is POSTed to the controller, the CreatedAt property will have the DateTime.Now filled in.
When you POST something, it has to go to an Action Method:
public class MyController : Controller
{
//other stuff
[HttpPost]
public ActionResult Edit(Product product)
{
product.CreatedAt // should equal the DateTime.Now set when you created the View
}
}
or you could set it in the controller after it POSTs:
public class MyController : Controller
{
//other stuff
[HttpPost]
public ActionResult Edit(Product product)
{
product.CreatedAt = DateTime.Now;
}
}
You may run into issues with Html.Hidden in this context, if you do, make sure to use the work around in place.

ASP.NET MVC 3 Adding comments in article view

I have article model with public ICollection<Comment> Comments { get; set; } and comment model. I have created view for article (Details view) and I want to show everything from model article (not problem) and comments to article to and after comments then show form for adding comment to article (not in other page, I want it in the view with article). For now I have this:
#model SkMoravanSvitavka.Models.Article
#{
ViewBag.Title = "Zobrazit";
}
<h2>Zobrazit</h2>
<fieldset>
<legend>Article</legend>
<div class="display-label">Title</div>
<div class="display-field">#Model.Title</div>
<div class="display-label">Text</div>
<div class="display-field">#Model.Text</div>
<div class="display-label">PublishedDate</div>
<div class="display-field">#String.Format("{0:g}", Model.PublishedDate)</div>
</fieldset>
#if (Model.Comments != null)
{
foreach (var comment in Model.Comments)
{
#Html.Partial("_Comment", comment)
}
}
<p>
#Html.ActionLink("Edit", "Edit", new { id = Model.ArticleID }) |
#Html.ActionLink("Back to List", "Index")
</p>
It shows article and there is partial view for all comments to article. And now I am not sure how to add form for adding comments. Thank you
Edit: Here is my comment controller and create methods (vytvorit = create in czech :) ):
public ActionResult Vytvorit(int articleID)
{
var newComment = new Comment();
newComment.articleID = articleID; // this will be sent from the ArticleDetails View, hold on :).
newComment.Date = DateTime.Now;
return View(newComment);
}
[HttpPost]
public ActionResult Vytvorit(Comment commentEntity)
{
db.Comments.Add(commentEntity);
db.SaveChanges();
return RedirectToAction("Zobrazit", "Clanek", new { id = commentEntity.articleID });
}
When I change #Html.RenderAction to #Html.Action it works. It is showing textbox for comment and I can add comment but there is problem that it not just add textbox but it add my site again (not just partial view but all view) and I am sure I add Create view for comment as partial.
Create a new Partial view, make it a strongly typed one of type Comment.
from the scaffolding templates, choose "Create" template.
handle the normal add new scenario of the comment.
add this Partial view to the Article details page.
Note that when you are about to save a new comment, you will need to get the hosting Article ID.
Hope it's now clear, if not, let me know.
update: assuming that you will add the "AddComment" partial view to your "Article details" view, you can do the following in order to add the comment.
1- Modify the GET (Create) action inside your CommentController as the following:
public ActionResult Create(int articleID)
{
var newComment = new CommentEntity();
newComment.articleID = articleID; // this will be sent from the ArticleDetails View, hold on :).
return View(newComment);
}
1- make the POST (Create) action like this:
[HttpPost]
public ActionResult Create(Comment commentEntity)
{
// Complete the rest..
}
2- The Partial view for the comment will look something like this:
#model NGO.Models.Comment
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<div class="addcommentbox">
<h2> Add Comment </h2>
#Html.TextAreaFor(model => model.Description)
<div class="ErrorMessage">
#Html.ValidationMessageFor(model => model.Description)
</div>
<input id="addComment" type="button" onclick="" value="Add" />
</div>
}
3- inside the ArticleDetails page, in the desired place that you need the add comment section to show, use RenderAction method to render the AddComment Partial view like the following:
Html.RenderAction("Create", "Comment",new {articleID = Model.ID});
the previous line will call the GET(Create) action inside CommentColtroller and pass the current Article ID, so the AddComment Partial view will come already populated with the current Article ID (and this is what we want).
that's it, feel free to ask me if it's not clear yet, and do let me know if it worked for you
I strongly recommend you to create view model for your article page. like below one;
public class ArticleViewModel {
public Article _article {get;set;}
//this is for the comment submit section
public Comment _comment {get;set;}
//this for the comments that you will view
public IQueryable<Comment> _comment {get;set;}
}
after that, pass this to your view from your controller and make your view strongly-typed to this ArticleViewModel class.
Create a section which is wrapped inside form tag as below;
#using(Html.BeginForm()){
#*Put your code inside here.
Create inputs for sending comments. like below;*#
#Html.TextboxFor(Model._comment.Content)
}
and then create a method for that;
[HttpPost]
[ActionName("name_of_your_article_page_action")]
public ActionResult Create(Comment commentEntity) {
}
NOTE : Of course, you do not need to create a seperate viewmodel. you can hardcode the name your inputs but this makes it hard to use validation and other kind of things. but not impossible to use validation though !

Resources