Is it a code smell to have to following pattern, given the following code (highly simplified to get straight to the point) ?
The models :
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public Category Cat { get; set; }
}
class Category
{
public int Id { get; set; }
public string Label { get; set; }
}
The view to edit a Product :
<% =Html.EditorFor( x => x.Name ) %>
<% =Html.EditorFor( x => x.Category ) %>
The EditorTemplate for Category
<% =Html.DropDownList<Category>() %>
The HtmlHelper method
public static MvcHtmlString DropDownList<TEntity>(this HtmlHelper helper)
where TEntity : Entity
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
return SelectExtensions.DropDownList(helper, "List", selectList, null, null);
}
For information, the real implementation of the helper method takes some lambdas to get the DataTextField and DataValueField names, the selected value, etc.
The point that bothers me is using a servicelocator inside the HtmlHelper. I think I should have a AllCategories property in my Product model, but I would need to be populated in the controller every time I need it.
So I think the solution I'm using is more straightforward, as the helper method is generic (and so is the modelbinder, not included here). So I just have to create an EditorTemplate for each type that needs a DropDownList.
Any advice ?
IMHO I'd leave it the way it is, have the same thing in another project.
BUT the service location bothered me as well so for another project I made this part of an ActionFilter which scans a model, finds all the anticipated dropdowns and does a batch load into ViewData. Since the ServiceLocator or Repository/Context/whatever is already injected into the Controller you don't have to spread your service location all over the place.
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
foreach( var anticipated in SomeDetectionMethod() )
{
var selectList = new SelectList(
ServiceLocator.GetInstance<SomethingGivingMe<TEntity>>().GetAll(),
"Id", "Label");
ViewData["SelectList." + anticipated.Label/Name/Description"] = selectList;
}
}
In the view you can then make a helper to load up those dropdowns via a custom editor template or other method.
advice: look at the asp.net mvc sample application from here: http://valueinjecter.codeplex.com/
good luck ;)
This is how ValueInjecter's Sample Application could get the dropdowns:
(but it doesn't right now cuz I'm ok with the Resolve thing)
public class CountryToLookup : LoopValueInjection<Country, object>
{
ICountryRepo _repo;
public CountryToLookup(ICountryRepository repo)
{
_repo = repo;
}
protected override object SetValue(Country sourcePropertyValue)
{
var value = sourcePropertyValue ?? new Country();
var countries = _repo.GetAll().ToArray();
return
countries.Select(
o => new SelectListItem
{
Text = o.Name,
Value = o.Id.ToString(),
Selected = value.Id == o.Id
});
}
}
Related
I have a table where I store all the different code/value keywords that I need in my app:
public class Keyword
{
public int id { get; set;}
public string name { get; set; }
public string valuecode { get; set; }
public string valuename { get; set; }
}
Then I use Keyword to store records like these
name valuecode valuename
.DealState 1 Draft
.DealState 2 Final
.DealState 3 Cancelled
.DealType NEW New Business
.DealType RNW Renewal
.DealType WFA Waiting for approval
Then in other models I have fields that are filled using these keywords. For example,
public class Deal
{
....
public string state { get; set; }
public string type { get; set; }
....
}
I have managed to have the fields filled with "valuecode" while displaying "valuename" in Create and Edit views (I use DropDownList with a SelectList built in the controller), but I cannot find a way to display valuename instead of valuecode in Index and Details views.
I'm trying to pass the same SelectList in the ViewBag for Index, but then I do not know which syntax to use in order to replace the "state" code with the state "description" for each record returned.
Any hint?
PS: I'm quite new to .net and mvc, usually work with RoR and ActiveRecord...
EDIT
In my KeywordController I have a method
public SelectList selectKeywordValues(string kwname, object selectedKeyword = null)
{
var keywordsQuery = from d in db.Keywords
where d.name == kwname
orderby d.valuename
select d;
SelectList kwlist = new SelectList(keywordsQuery, "valuecode", "valuename", selectedKeyword);
return kwlist;
}
Then in my DealController i have the index method
public ActionResult Index()
{
var kw = new KeywordController();
ViewBag.state = kw.selectKeywordValues(".DealState");
return View(db.Deals.ToList());
}
SOLVED
In DealController the index method is the following
public ActionResult Index()
{
var kw = new KeywordController();
SelectList states = kw.selectKeywordValues(".DealState");
SelectList types = kw.selectKeywordValues(".DealType");
foreach (var item in db.Deals.ToList())
{
SelectListItem mystate = states.Where(row => row.Value == item.state).ElementAt(0);
SelectListItem mytype = types.Where(row => row.Value == item.type).ElementAt(0);
item.state = mystate.Text;
item.type = mytype.Text;
}
return View(db.Deals.ToList());
}
Now the db.Deals.ToList() is filled with descriptions and not with codes.
You can define a view model called DealViewModel that contains DealState and DealType properties. Then populate the DealViewModel with joins in LINQ before passing to the views that reference the view model.
Another approach is to use enums in EF5.
I am using C#, MVC3, EF5, SQL Server 2008 R2.
I have an intersection table ie
Lecturer -< LecturerCourse >- Course
The list of Lecturers are populated.
When I add a course, it would be neat to have a list of Lecturers that I could select from, that teach the course in question. When I save the new Course record, this multiselect also should save its data back to the "LecturerCourse" table via Model Binding.
I am using EF5.
Can you recommended a simple and standard approach to solving CRUD for a join, ie "LecturerCourse", table? I have looked online, but some of the approaches seem very complicated.
Many thanks.
Alright, it's going to be a long one. To allow this to happen in "one page" (through POST, or you could use Ajax, technically), you need a combination of a Get and Post version of the method and to construct your view model correctly. Below are the classes that I will use for demonstration purposes:
public class NewCourse
{
[Required]
public string Name { get; set; }
// And your other properties
public int[] LecturerIds { get; set; }
}
public class ViewLecturer
{
public int Id { get; set; }
public int Name { get; set; }
}
public class NewCourseViewModel
{
public NewCourse Course { get; set; }
public IEnumerable<ViewLecturer> Lecturers { get; set; }
}
NewCourseViewModel will be the model for the View (see below). ViewLecturer will give you a lighter mapping between your available Lecturer and the information required to Add to them.
As for the Controller:
public class CourseController : Controller, IDisposable
{
private Lazy<YourContext> lazyContext =
new Lazy<YourContext>(() => new YourContext());
private YourContext Context
{
get { return lazyContext.Value; }
}
public ActionResult New()
{
var model = new NewCourseViewModel {
Course = new NewCourse(),
Lecturers = Context.Lecturers
.Select(l => new ViewLecturer { Id = l.Id, Name = l.Name })
};
return View(model);
}
[HttpPost]
public ActionResult New(NewCourse course)
{
if(ModelState.IsValid)
{
var lecturers = course.Lecturers
.Select(l => new Lecturer { Id = l.Id })
.ToList();
foreach(var lecturer in lecturers)
Context.Lecturers.Attach(lecturer);
var newCourse = new Course {
Name = course.Name,
// ... and the rest of the mapping
Lecturer = lecturers
};
context.Courses.Add(newCourse);
context.SaveChanges();
// Could have to handle DbUpdateException if you want
return RedirectToAction(...);
}
return View(new NewCourseViewModel {
Course = course,
Lecturers = Context.Lecturers
.Select(l => new ViewLecturer { Id = l.Id, Name = l.Name })
});
}
public void Dispose()
{
if(lazyContext.IsValueCreated)
lazyContext.Value.Dispose();
}
}
Your first New method will give you the entry point for your Course creation page. The rest of the validation and actual adding will be done through the [HttpPost]overload. As for your View (that should be in the ~/Views/Course/New.cshtml):
#model NewCourseViewModel
// ... Then when you are ready to begin the form
#using(Html.BeginForm("New", "Course", FormMethod.Post))
{
// Your List of Lecturers
#Html.ListBoxFor(m => m.Course.LecturerIds,
new MultiSelectList(
Model.Lecturers,
"Id",
"Name",
m.Course.LecturerIds ?? new int[0]
))
// Your Other Model binding
}
When the submit button will be pressed, the action matched will be the New(NewCourse course). The names are important because of the way the HtmlHelpers generate their Ids. Because we are only included one property of the whole view model, it will match the parameter name course based on the view model's Course property. You will get a list of Ids for the Lecturers which you will be able to use to attach to the DbContext and add directly to the new Course model (Entity Framework will do the rest). In cases where there was a problem, we can get back the list of lecturers and re-use the same NewCourse in the view model.
Now this is example is very basic but it should give you a good starting point as to how you can structure your view model.
I've read many articles which they state that querying should not be placed in the Controller, but I can't seem to see where else I would place it.
My Current Code:
public class AddUserViewModel
{
public UserRoleType UserRoleType { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
public ActionResult AddUser()
{
AddUserViewModel model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
The View:
<li>#Html.Label("User Role")#Html.DropDownListFor(x => Model.UserRoleType.UserRoleTypeID, Model.UserRoleTypes)</li>
How do I retain the View Model and Query and exclude the User Type that should not show up?
I think that you are doing it just fine.
Any way... all you can do to remove the querying logic from controller is having a ServiceLayer where you do the query and return the result.
The MVC pattern here is used correctly... what your are lacking is the other 2 layers (BusinessLayer and DataAccessLayer)... since ASP.NET MVC is the UI Layer.
UPDATE, due to comment:
Using var userroletypes = db.UserRoleTypes.Where(u=> u.UserRoleType != 1);
is OK, it will return a list of UserRoleType that satisfy the query.
Then, just create a new SelectList object using the userroletypes collection... and asign it to the corresponding viewmodel property. Then pass that ViewModel to the View.
BTW, I never used the db.XXXX.Select() method before, not really sure what it does... I always use Where clause.
SECOND UPDATE:
A DropDownList is loaded from a SelectList that is a collection of SelectItems.
So you need to convert the collection resulting of your query to a SelectList object.
var userroletypes = new SelectList(db.UserRoleTypes.Where(u=> u.UserRoleType != 1), "idRoleType", "Name");
then you create your ViewModel
var addUserVM = new AddUserViewModel();
addUserVM.UserRoleTypes = userroletypes;
and pass addUserVM to your view:
return View(addUserVM );
Note: I'm assuming your ViewModel has a property of type SelectList... but yours is public IEnumerable<SelectListItem> UserRoleTypes { get; set; } so you could change it or adapt my answer.
I don't see anything wrong with your code other than this db instance that I suppose is some concrete EF context that you have hardcoded in the controller making it impossible to unit test in isolation. Your controller action does exactly what a common GET controller action does:
query the DAL to fetch a domain model
map the domain model to a view model
pass the view model to the view
A further improvement would be to get rid of the UserRoleType domain model type from your view model making it a real view model:
public class AddUserViewModel
{
[DisplayName("User Role")]
public string UserRoleTypeId { get; set; }
public IEnumerable<SelectListItem> UserRoleTypes { get; set; }
}
and then:
public ActionResult AddUser()
{
var model = new AddUserViewModel()
{
UserRoleTypes = db.UserRoleTypes.Select(userRoleType => new SelectListItem
{
Value = SqlFunctions.StringConvert((double)userRoleType.UserRoleTypeID).Trim(),
Text = userRoleType.UserRoleTypeName
})
};
return View(model);
}
and in the view:
#model AddUserViewModel
<li>
#Html.LabelFor(x => x.UserRoleTypeId)
#Html.DropDownListFor(x => x.UserRoleTypeId, Model.UserRoleTypes)
</li>
I have a ViewModel with a Filter property that has many properties that I use to filter my data
Example:
class MyViewModel : IHasFilter
{
public MyData[] Data { get; set; }
public FilterViewModel Filter { get; set; }
}
class FilterViewModel
{
public String MessageFilter { get; set; }
//etc.
}
This works fine when using my View. I can set the properties of Model.Filter and they are passed to the Controller. What I am trying to do now, is create an ActionLink that has a query string that works with the above format.
The query string generated by my View from above looks like this:
http://localhost:51050/?Filter.MessageFilter=Stuff&Filter.OtherProp=MoreStuff
I need to generate an ActionLink in a different View for each row in a grid that goes to the View above.
I have tried:
Html.ActionLink(
item.Message,
"Index",
"Home",
new { Filter = new { MessageFilter = item.Message, }, },
null);
I also tried setting the routeValues argument to:
new MyViewModel { Filter = new FilterViewModel { MessageFilter = item.Message, }, },
But these do not generate the query string like the above one.
Interesting question (+1). I'm assuming that the purpose is to use the default model binder to bind the querystring parameters to to your Action parameters.
Out of the box I do not believe that the ActionLink method will do this for you (of course there is nothing stopping you from rolling your own). Looking in reflector we can see that when the object is added to the RouteValueDictionary, only key value pairs are added. This is the code that adds the key value pairs and as you can see there is no traversing the object properties.
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
{
object obj2 = descriptor.GetValue(values);
this.Add(descriptor.Name, obj2);
}
So for your object
var values = new { Filter = new Filter { MessageFilter = item.Message } }
the key being added is Filter and the value is your Filter object which will evaluate to the the fully qualified name of your object type.
The result of this is Filter=Youre.Namespace.Filter.
Edit possible solution depending on your exact needs
Extension Method does the work
Note that it uses the static framework methods ExpressionHelper and ModelMetadata (which are also used by the existing helpers) to determine the appropriate names that the default model binder will understand and value of the property respectively.
public static class ExtentionMethods
{
public static MvcHtmlString ActionLink<TModel, TProperty>(
this HtmlHelper<TModel> helper,
string linkText,
string actionName,
string controllerName,
params Expression<Func<TModel, TProperty>>[] expressions)
{
var urlHelper = new UrlHelper(helper.ViewContext.HttpContext.Request.RequestContext);
var url = urlHelper.Action(actionName, controllerName);
if (expressions.Any())
{
url += "?";
foreach (var expression in expressions)
{
var result = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, helper.ViewData);
url = string.Concat(url, result, "=", metadata.SimpleDisplayText, "&");
}
url = url.TrimEnd('&');
}
return new MvcHtmlString(string.Format("<a href='{0}'>{1}</a>", url, linkText));
}
}
Sample Models
public class MyViewModel
{
public string SomeProperty { get; set; }
public FilterViewModel Filter { get; set; }
}
public class FilterViewModel
{
public string MessageFilter { get; set; }
}
Action
public ActionResult YourAction(MyViewModel model)
{
return this.View(
new MyViewModel
{
SomeProperty = "property value",
Filter = new FilterViewModel
{
MessageFilter = "stuff"
}
});
}
Usage
Any number of your view model properties can be added to the querystring through that last params parameter of the method.
#this.Html.ActionLink(
"Your Link Text",
"YourAction",
"YourController",
x => x.SomeProperty,
x => x.Filter.MessageFilter)
Markup
<a href='/YourAction/YourController?SomeProperty=some property value&Filter.MessageFilter=stuff'>Your Link Text</a>
Instead of using string.Format you could use TagBuilder, the querystring should be encoded to be safely passed in a URL and this extension method would need some additional validation but I think it could be useful. Note also that, though this extension method is built for MVC 4, it could be easily modified for previous versions. I didn't realize that that one of the MVC tags was was for version 3 until now.
You could create one RouteValueDictionary from a FilterViewModel instance and then use ToDictionary on that to pass to another RouteValues with all the keys prefixed with 'Filter.'.
Taking it further, you could construct a special override of RouteValueDictionary which accepts a prefix (therefore making it more useful for other scenarios):
public class PrefixedRouteValueDictionary : RouteValueDictionary
{
public PrefixedRouteValueDictionary(string prefix, object o)
: this(prefix, new RouteValueDictionary(o))
{ }
public PrefixedRouteValueDictionary(string prefix, IDictionary<string, object> d)
: base(d.ToDictionary(kvp=>(prefix ?? "") + kvp.Key, kvp => kvp.Value))
{ }
}
With that you can now do:
Html.ActionLink(
item.Message,
"Index",
"Home",
new PrefixedRouteValueDictionary("Filter.",
new FilterViewModel() { MessageFilter = item.Message }),
null);
The caveat to this, though, is that the Add, Remove, TryGetValue and this[string key] methods aren't altered to take into account the prefix. That can be achieved by defining new versions of those methods, but because they're not virtual, they'd only work from callers that know they're talking to a PrefixedRouteValueDictionary instead of a RouteValueDictionary.
I would like to use Linq and strongly typed views in the right way. at the moment I do the following:
Make a Model to verify agianst:
public class Menu
{
public int Id { get; private set; }
public string Text { get; private set; }
public string Action { get; private set; }
public string Controller { get; private set; }
public string Parameter { get; private set; }
public string Langue { get; private set; }
public Menu(int id, string controller, string action, string parameter, string text)
{
Id = id;
Controller = controller;
Action = action;
Text = text;
Parameter = parameter;
}
Use Linq to get the data from the database into the model:
public static List<Menu> GetTabListForMenu(string langue)
{
Page_dbEntities entity = new Page_dbEntities();
var tabList = (from ml in entity.wpmenulangue
where ml.Langue == langue
from m in entity.wpmenu
where ml.Menu == m.Id
from l in entity.wplangue
where ml.Langue == l.Langue
from p in entity.wppage
where p.Id == m.Page
select new { m.Id, p.Controller, p.Action, p.Parameter, ml.Text}).ToList();
List<Menu> menu = new List<Menu>();
foreach (var item in tabList)
{
menu.Add(new Menu(item.Id, item.Controller, item.Action, item.Parameter, item.Text));
}
return menu;
}
I am pretty convinced that this is not the optimal way to do this and have 2 questions:
When I get the data from the database I first use a var and then have to move it to the object with a foreach. this seems like a waste of both my time and less effeicent then getting it with sql.
I have been told that I can just verify up agianst the entitymodel. Even if i use multiple entities in a view. is this true? (the one telling me this wes not able to get it to work and I have not been able to find anything about it online).
I will try to look back on this post in the next couple of hours, but might have to wait 24 hours.
public static List<Menu> GetTabListForMenu(string langue)
{
Page_dbEntities entity = new Page_dbEntities();
return (from ml in entity.wpmenulangue
where ml.Langue == langue
from m in entity.wpmenu
where ml.Menu == m.Id
from l in entity.wplangue
where ml.Langue == l.Langue
from p in entity.wppage
where p.Id == m.Page
select new Menu(m.Id, p.Controller, p.Action, p.Parameter, ml.Text)
).ToList();
}
As for the validation is concerned you shouldn't use multiple entities in the view. You should use a single entity which is called ViewModel. This ViewModel is a class that represents the data on the view. If you are using DataAnnotations for validation you could decorate this view model properties with attributes that indicate how to be validated.