Is it possible to assign multiple actions to the same controller method? - c#

I have a simple MVC Controller that returns a list of files from folders based on View action. Index() action contains a list of Collections so then user clicks on CollectionOne, the corresponding view is populated. The same behavior is applied to other collections.
The problem is, I have a lot redundant code that I have been able to manage at certain degree by using a private ActionContent() method invoked by all actions. So whenever I have a new collection to add to website, I just add a ActionResult for this Collection and invoke ActionContent() method.
Is there any way to optimize this code to make it more dynamic, without adding a new ActionResult every time a need to post a new collection?
My controller looks like this:
public class PortfolioController : Controller
{
public ActionResult CollectionOne()
{
return View(ActionContent());
}
public ActionResult CollectionTwo()
{
return View(ActionContent());
}
private IEnumerable<string> ActionContent()
{
const string folder = #"~/Content/images/portfolio/";
var path = folder + ControllerContext.RouteData.Values["action"];
var files = Directory
.EnumerateFiles(Server.MapPath(path))
.Select(Path.GetFileName);
return files;
}
}
I want to turn it into something like this (to avoid redundancy) using ActionNames or maybe proper route mapping:
public class PortfolioController : Controller
{
[ActionName("CollectionOne")]
[ActionName("CollectionTwo")]
[ActionName("CollectionThree")]
public ActionResult PortfolioCollection()
{
const string folder = #"~/Content/images/portfolio/";
var path = folder + ControllerContext.RouteData.Values["action"];
var files = Directory
.EnumerateFiles(Server.MapPath(path))
.Select(Path.GetFileName);
return View(files);
}
}

That's what parameters are for:
public ActionResult PortfolioCollection(string id)
{
const string folder = #"~/Content/images/portfolio/";
var files = Directory
.EnumerateFiles(Server.MapPath(folder + id))
.Select(Path.GetFileName);
return View(files);
}
You can make a custom route to assign any URL pattern you want.

Related

Viewengine using a static class on controller view method

I am working on themes at present and the majority of work is going well am just at the phase where I am figuring out what view to present to the user.
This would be on a controller method so far I have.
public static class ThemeViewExtensions
{
public static string GetViewPath(RazorView view , string viewName = "Default")
{
var theme = "Default";
var themeViewPath = $"/Themes/{theme}{view.ViewPath}/{viewName}.cshtml";
return viewPath;
}
}
But for some reason I cannot access it here yes I have my using statement in
public IActionResult Index()
{
return View(this.GetViewPath());
}
What I want to happen is to be able to use it as above and be able to get the ViewPath and name that is being called for example /Controller/Action/ViewName whatever cshtml is that possible on a view?

Asp.net core Cleanest way to return View or Json/XML

In asp.net core I would like to set up my API controller to do the following:
by default return View(model);
/api/id.json to return model; as json
/api/id.xml to return model; as xml
The second two can be achieved by using the [FormatFilter] see here
[FormatFilter]
public class ProductsController
{
[Route("[controller]/[action]/{id}.{format?}")]
public Product GetById(int id)
However this requires the method to return an object and not a View(object). Is there anyway to cleanly support also returning Views?
You cannot do both in the same action. However, you can factor out the common functionality into a private method and then implement two actions with minimal code duplication:
[Route("[controller]")]
[FormatFilter]
public class ProductsController : Controller
{
private Product GetByIdCore(int id)
{
// common code here, return product
}
[HttpGet("[action]/{id}")]
[ActionName("GetById")]
public IActionResult GetByIdView(int id) => View(GetByIdCore(id));
[HttpGet("[action]/{id}.{format}")]
public Product GetById(int id) => GetByIdCore(id);
}
It's necessary to use different action names here, because the method signatures cannot differ merely on return type. However, the [ActionName] attribute can be used as above to make them appear to have the same name for the purposes of URL generation and such.
You can actually achieve this just using the one action. Here's an example of how I got it to work:
[FormatFilter]
public class ProductsController : Controller
{
[Route("[controller]/[action]/{id}.{format?}")]
public IActionResult GetById(int id, string format)
{
var yourModel = ...;
if (string.IsNullOrWhiteSpace(format))
return View(yourModel);
return Ok(yourModel);
}
By using IActionResult as the return type, you can return either a ViewResult or an OkObjectResult. You can get access to the format value by taking it as a parameter in your action, check if it's empty and then react accordingly.
I also added Controller as the base class in order to access the convenience methods for creating the relevant results (View(...) and Ok(...)).
If you're going to be using this pattern a lot, to keep your controllers as clean as possible, you could create a base class that exposed a "FormatOrView" method:
[FormatFilter]
public abstract class FormatController : Controller
{
protected ActionResult FormatOrView(object model)
{
var filter = HttpContext.RequestServices.GetRequiredService<FormatFilter>();
if (filter.GetFormat(ControllerContext) == null)
{
return View(model);
}
else
{
return new ObjectResult(model);
}
}
}
And then your controller can inherit from this and use the FormatOrView method
public class ProductsController : FormatController
{
[Route("[controller]/[action]/{id}.{format?}")]
public ActionResult GetById(int id)
{
var product = new { Id = id };
return FormatOrView(product);
}
}
Edit to list final accepted answer by GreyCloud: Here is a generic slightly simplified method you can put into a controller (or make an extension method or put into an abstract base class as above). Note the ?. in case the service is not defined for some reason.
private ActionResult<T> FormatOrView<T>(T model) {
return HttpContext.RequestServices.GetRequiredService<FormatFilter>()?.GetFormat(ControllerContext) == null
? View(model)
: new ActionResult<T>(model);
}
The FormatFilter is part of the content negotiation of your app, in AspNetCore, you have the control to handle your input or output formatters also on the ConfigureServices where you have more control, even you can add more media types there
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options .OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
options .InputFormatters.Add(new XmlDataContractSerializerInputFormatter(options ));
//more output formatters
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
if (jsonOutputFormatter != null)
{
jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.myvendormediatype");
}
}
}
But going back on the content negotiation in your controllers you can keep just one. The only thing is that you need to know the mediaType to return your View or your json content. Only be sure to pass an accept header with the content type you want. With the content type you are defining for an api or for an mvc application which is the content/format the client should expect
[HttpGet("[action]/{id}")]
public IActionResult public Product GetById(int id, [FromHeader(Name = "Accept")] string mediaType)
{
if (mediaType == "application/vnd.myvendormediatype")
{
var data = GetYourData(...)
return Json(data);
}
else return View("YourDefaultView");
}

Create multiple views and access with single action in controller

i am having my views separated under subfolder, i have only one Action method in controller, i have my view names in the database, when i call the action method by passing the parameter (for instance: id = 1), it will fetch the view name from the database and the respective view will be load.
public ActionResult Index(int FormId)
{
var getViews = db.fetchViews.where id = 1; //get views from db
return view(getviews.viewName);
}
This is my views in the solution.
while i call the view from action method it says unable to find the views.
i cannot hardcode the subfolder in the Action method like this,
return View("~/Views/Form/Customer1/getviews.viewName");
any ideas would be appreciate...
If you can't move the views to the directory that the razor view engine is looking for, you can try this:
public ActionResult Index(int FormId)
{
var getViews = db.fetchViews.where id = 1; //get views from db
var viewpath = string.Format("~/Views/Form/{0}/{0}", getviews.viewName);
return View(viewpath);
}
Alternatively, you could create a custom RazorViewEngine that could search for the view in a subdirectory of the same name.
This post has a method of adding search view locations to the razor view engine, your custom search view format would be something like this:
new string[]
{
"~/Views/{1}/{0}/{0}.cshtml"
}
Like this?
public ActionResult Index(int FormId)
{
var viewName = "~/Views/Form/Customer" + FormId + "/customer" + FormId;
return View(viewName);
}
If you want the searching of views to be according to what you want, you can inherit the RazorViewEngine and modify the path where Razor will look at. You may see this SO post

View to String from another controller

i have done as Vdex suggested here:
https://stackoverflow.com/a/5801502/973485
And used the RenderPartialToString method he found. And it works perfectly like this:
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Test()
{
string t = ViewToString.RenderPartialToString("Index", null, ControllerContext);
return Content(t);
}
}
But if i want to render the Home > Index from another Controller, i get:
Value cannot be null.
Parameter name: controllerContext
Like this:
public class FooController : Controller
{
public ActionResult Index()
{
string t = ViewToString.RenderPartialToString("Index", null, new HomeController().ControllerContext);
return Content(t);
}
}
Is there any way to pass a View from another Controller to a string? I have tried many different methods, and it all of them fails at the ControllerContext. Many thanks!
Update: Why i need to do this:
Imagine i have a website full of widgets, the amount of widgets on each page is dynamic, so i cannot hardcode them in my cshtml file. But in that file there are different areas defined where the widgets gets printet out. To print out these widget i have a list of IWidgetController wich contains alle the different Widgets available, and the interface sais that they need to containe a ActionResult for edit, new and view. example of widgets: CalenderController, NewsController, GalleryController and so on... So in those areas i need to print out the content of each of those Controllers. Now i could also load the URLHTML but i figured doing it from the inside would be faster... right?
Try this:
string t = ViewToString.RenderPartialToString("Index", null, this.ControllerContext);
Anyway, why do you need to convert to a string?

How to pass Form Values to the Controller in .NET MVC

In ASP.net MVC:
How should/Can I pass Form data (From the View) to the Controller?
This is the way I am heading :
The Controller Index function is passing a ViewModel object to the View.
The ViewModel object contains a paginated list as well as some SelectLists.
_ The ViewModel object also contains a custom class I named theFilter. This class' purpose is to hold the Filter information Posted from the View via a Form.
I want the Index [AcceptVerbs(HttpVerbs.Post)] function to receive theFilter object populated with the form data, as well as the page number (as is it right now)
Here are snippets of my code:
The Controller/Index postback function:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(int? page, CaseFilter caseFilter)
{
const int pageSize = 10;
var cases = caseRepo.FindAllCases();
var paginatedCases = new PaginatedList<Case>(cases, page ?? 0, pageSize);
return View(new CaseIndexViewModel(paginatedCases, caseFilter));
}
The Filter Class:
public class CaseFilter
{
int iVolume_id = 0,
iSubject_id = 0;
public CaseFilter() {
}
public int Volume_id { get { return iVolume_id; } set { iVolume_id = value; } }
public int Subject_id { get { return iSubject_id; } set { iSubject_id = value; } }
}
And the ViewModel class:
public class CaseIndexViewModel
{
public PaginatedList<Case> PaginatedCases { get; private set; }
public CaseFilter CaseFilter { get; private set; }
public CaseIndexViewModel(PaginatedList<Case> paginatedCases, CaseFilter caseFilter)
{
PaginatedCases = paginatedCases;
CaseFilter = caseFilter;
}
}
Basically I am trying to avoid using Request.Form to populate the Filter class, at least not to use it within the Controller.
Any help, suggestions or disses are welcome!
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(FormCollection collection)
{
string valueFromNameTextBox = collection["name"];
}
You can index into this collection with the names of all the inputs on the form.
Rather than complicate my method signatures, I've taken to using the ValueProvider property and Try/UpdateModel in the Controller to retrieve form/route values unless the values are simple properties. On the other hand, I would probably also not make the filter part of the model for the View -- I tend to have a narrower conception of the model for the page, wanting it rather to be the business model rather that a model of all the data on the page -- and would simply pass the filter values via ViewData.
To expand BFree's answer, you can go through all the elements in the form by doing something like this:
foreach (string key in collection.keys) {
if (key.contains("blah"))
text1 = collection[key];
}
If it has too many elements for the key.contains if, it can get a bit ugly though, so beware ;).
Finally, I do not need to even use the Request Collection. The CaseFilter object is filled automatically as I set it as a parameter in
public ActionResult Index(int? page, CaseFilter caseFilter)
The code above works as it is.

Categories