I am using the latest version Telerik MVC controls. I am using ASP.NET MVC 3 with razor.
I have a grid that lists all of my grant applications. I am wanting to use a grid that loads these grant applications via AJAX. I also need to create a client template column that has action links. These action links can vary depending on the state of each grant application.
I worked through the article at: http://gedgei.wordpress.com/2011/07/02/telerik-mvc-grid-actionlink-column/. I implemented the code as is and it works, I can create a client template column with a link in it. In my scenario I need to be able to pass in 2 parameters to the helper method, like:
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
How I eventually implement this method in the end will change, but for now I am playing with these 2 input parameters to see how they are passed through and how I can retrieve them in the helper method.
The first question that I have regarding the article, why does the writer do the following:
var builder = factory.Template(x =>
{
var actionUrl = urlHelper.Action(action, controller, routeValues.Compile().Invoke(x));
return string.Format(#"{1}", actionUrl, linkText);
});
I can only assume that this is the server side template that is created? But nothing displays in the grid, so how do I skip this part and go directly to the client template (this is what I actually need).
The following part is also confusing because when the first parameter (id) check comes through then it is of type ParameterExpression so it goes into the true part of the if, but when the second parameter (grant application state id) comes in then it is of another type (not sure what) so then it goes into the false part of the if statement:
switch (argument.NodeType)
{
case ExpressionType.Constant:
value = ((ConstantExpression)argument).Value;
break;
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression)argument;
if (memberExpression.Expression is ParameterExpression)
value = string.Format("<#= {0} #>", memberExpression.Member.Name);
else
value = GetValue(memberExpression);
break;
default:
throw new InvalidOperationException("Unknown expression type!");
}
When the second paramenter values goes into the false part of the if statement it fails here:
value = GetValue(memberExpression);
..and gives the following error message which I have no idea what it is:
variable 'item' of type MyProject.ViewModels.GrantApplicationListViewModel' referenced from scope '', but it is not defined
Here is my view model:
public class GrantApplicationListViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullNameDisplay
{
get { return FirstName + " " + LastName; }
}
public DateTime CreatedDate { get; set; }
public GrantApplicationStateType GrantApplicationStateType { get; set; }
}
Here is my partial grid declaration in my view:
#(Html.Telerik()
.Grid<GrantApplicationListViewModel>()
.Name("grdGrantApplications")
.Columns(column =>
{
column.Bound(x => x.Id)
.ClientTemplate(
"<label class=\"reference-number\">" + "<#= Id #>" + "</label>"
)
.Title("Ref #")
.Width(70);
column.Bound(x => x.FullNameDisplay)
.Title("Owner")
.Width(200);
column.Bound(x => x.GrantApplicationStateType.Name)
.Title("Status")
.Width(90);
//column.ActionLink("Edit", "Edit", "GrantApplication", item => new { id = item.Id });
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
})
.DataBinding(dataBinding => dataBinding.Ajax().Select("AjaxGrantApplicationsBinding", "Home"))
.Pageable(paging => paging.PageSize(30))
.TableHtmlAttributes(new { #class = "telerik-grid" })
)
What I am trying to achieve with the above is code is something to the effect of:
if grant application id = 1
then return Edit link and View link
else
then return Details link
How would I do the above? Is the code in that article the only way to do it? Isn't there a more simplar way? I did Google and couldn't find much help on what I want to do. Has any one else come across something like this?
If all you want is the client template to display different content based on the application id, it would be simpler to just put a conditional in the client template.
column.Bound(x => x.Id)
.ClientTemplate("<# if (Id == 1 ) { #> Edit Link and View Link <# } else { #> Details Link <# } #>");
The Edit, View, and Details links would be put in the same way they are put in without the conditional.
Related
I am trying to create a more dynamic approach to pulling data for a view than a switch statement. Right now I have several different options and more could be added anytime. The tables that will be pulled from are all the same in format except for the name of the table and the name of their ID field.
public List<listoftables> BuildListOfTables(string router)
{
var listOfViewModels = new List<FormatOfTables>();
using var context = new TableContext();
switch (router)
{
case "firstTable":
listOfViewModels = context.Set<firstTable>().Select(x => new FormatOfTables
{
UniqueID = x.FirstTableID,
Value = x.Value,
}).ToList();
break;
case "secondTable":
listOfViewModels = context.Set<secondTable>().Select(x => new FormatOfTables
{
UniqueID = x.SecondTableID,
Value = x.Value,
}).ToList();
break;
case "thirdTable":
listOfViewModels = context.Set<ThirdTable>().Select(x => new FormatOfTables
{
UniqueID = x.ThirdTableID,
Value = x.Value,
}).ToList();
break;
return listOfViewModels;
}
I'm trying to find a way to do this more dynamically. So as long as the option in router matches a table name, each table that gets put into the model just fills the UniqueID and Value to be whatever their ID and value happens to be rather than having to match the column names. So if a fourth table came in I would only have to worry about if router matched the table name rather than having to add an entirely new switch per entry.
The tables that will be pulled from are all the same in format except for the name of the table and the name of their ID field.
The problem here is passing the typename to the Set<T>() function. But we'll get to that. First, let's make some of this easier by adding an interface:
public interface IMyTable
{
string TableName {get;}
int UniqueID {get;}
}
Then each of your firstTable, secondTable, ThirdTable types must implement this interface:
public class firstTable : IMyTable
{
// existing class stuff here
public int UniqueID { get { return FirstTableId;} }
public string TableName { get { return "FirstTable"; } }
}
And now the method can look like this:
public IEnumerable<listoftables> BuildListOfTables(string router)
{
using var context = new TableContext();
DBSet tableSet = null; //I'm making an assumption about the Set() function here. You may need to change the type.
switch (router)
{
case "firstTable":
tableSet = context.Set<firstTable>();
break;
case "secondTable":
tableSet = context.Set<secondTable>();
break;
case "thirdTable":
tableSet = context.Set<ThirdTable>();
break;
}
if (tableSet != null)
{
return tableSet.Select(x => new FormatOfTables
{
UniqueID = x.UniqueID,
Value = x.Value
});
}
return null;
}
This reduces the repeated boilerplate down to just as much as is necessary to call the generic Set<>() function.
From here we can further reduce the code by changing how the function is designed, including how you expect to call it:
public IEnumerable<listoftables> BuildListOfTables<T>() where T : IMyTable
{
using var context = new TableContext();
return context.Set<T>()
.Select(x => new FormatOfTables
{
UniqueID = x.UniqueID,
Value = x.Value
});
}
But all this really does is push where you have to put the switch() statement up to the call site. However, that might be worth it if the call site happens to have the type information already available.
Note for ALL of these examples I converted the method to return IEnumerable instead of a List. Calling .ToList() can be notoriously bad for performance. If you really need a list (hint: you usually don't) you can still put the ?.ToList() after the function call. You might be able to improve things even further in this case by returning IQueryable, which could let later code continue the expression tree before executing anything on the server.
I have a dynamic list of dynamic lists, which have <input />s that need to be POSTed to an MVC controller/action and bound as a typed object. The crux of my problem is I can't figure out how to manually pick out arbitrary POSTed form values in my custom model binder. Details are below.
I have a list of US States that each have a list of Cities. Both States and Cities can be dynamically added, deleted, and re-ordered. So something like:
public class ConfigureStatesModel
{
public List<State> States { get; set; }
}
public class State
{
public string Name { get; set; }
public List<City> Cities { get; set; }
}
public class City
{
public string Name { get; set; }
public int Population { get; set; }
}
The GET:
public ActionResult Index()
{
var csm = new ConfigureStatesModel(); //... populate model ...
return View("~/Views/ConfigureStates.cshtml", csm);
}
The ConfigureStates.cshtml:
#model Models.ConfigureStatesModel
#foreach (var state in Model.States)
{
<input name="stateName" type="text" value="#state.Name" />
foreach (var city in state.Cities)
{
<input name="cityName" type="text" value="#city.Name" />
<input name="cityPopulation" type="text" value="#city.Population" />
}
}
(There is more markup and javascript, but I leave it out for brevity/simplicity.)
All form inputs are then POSTed to server, as so (parsed by Chrome Dev Tools):
stateName: California
cityName: Sacramento
cityPopulation: 1000000
cityName: San Francisco
cityPopulation: 2000000
stateName: Florida
cityName: Miami
cityPopulation: 3000000
cityName: Orlando
cityPopulation: 4000000
I need to capture the form values, ideally bound as a List<State> (or, equivalently, as a ConfigureStatesModel), as so:
[HttpPost]
public ActionResult Save(List<State> states)
{
//do some stuff
}
A custom model binder seems like the right tool for the job. But I don't know how to know which city names and city populations belong to which state names. That is, I can see all the form keys and values POSTed, but I don't see a way to know their relation:
public class StatesBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//California, Florida
List<string> stateNames = controllerContext.HttpContext.Request.Form.GetValues("stateName").ToList();
//Sacramento, San Francisco, Miami, Orlando
List<string> cityNames = controllerContext.HttpContext.Request.Form.GetValues("cityName").ToList();
//1000000, 2000000, 3000000, 4000000
List<int> cityPopulations = controllerContext.HttpContext.Request.Form.GetValues("cityPopulation")
.Select(p => int.Parse(p)).ToList();
// ... build List<State> ...
}
}
If I could just know the order all values came in in relation to all other form values, that would be enough. The only way I see to do this is looking at the raw request stream, as so:
Request.InputStream.Seek(0, SeekOrigin.Begin);
string urlEncodedFormData = new StreamReader(Request.InputStream).ReadToEnd();
but I don't want to be messing with manually parsing that.
Also note that the order of the list of states and the order of the lists of cities in each state matter, as I persist the concept of display-order for them. So that would need to be preserved from the form values as well.
I've tried variations of dynamic list binding like this and this. But it feels wrong junking up the html and adding a lot of (error-prone) javascript, just to get the binding to work. The form values are already there; it should just be a matter of capturing them on the server.
The only obvious way I see of building a form that will actually represent which cities belong to which state would require that you use the strongly-typed helpers.
So, I'd use something similar to:
#model Models.ConfigureStatesModel
#for (int outer = 0; outer < Model.States.Count; outer++)
{
<div class="states">
#Html.TextBoxFor(m => m.States[outer].Name, new { #class="state" })
for (int inner = 0; inner < Model.States[outer].Cities.Count; inner++)
{
<div class="cities">
#Html.TextBoxFor(m => m.States[outer].Cities[inner].Name)
#Html.TextBoxFor(m => m.States[outer].Cities[inner].Population)
</div>
}
</div>
}
This will create inputs with form names that the default modelbinder can handle.
The part that requires some additional work is handling the re-ordering. I would use something like this, assuming you are using jQuery already:
// Iterate through each state
$('.states').each(function (i, el) {
var state = $(this);
var input = state.find('input.state');
var nameState = input.attr('name');
if (nameState != null) {
input.attr('name', nameState.replace(new RegExp("States\\[.*\\]", 'gi'), '[' + i + ']'));
}
var idState = input.attr('id');
if (idState != null) {
input.attr('id', idState.replace(new RegExp("States_\\d+"), i));
}
// Iterate through the cities associated with each state
state.find('.cities').each(function (index, elem) {
var inputs = $(this).find('input');
inputs.each(function(){
var cityInput = (this);
var nameCity = cityInput.attr('name');
if (nameCity != null) {
cityInput.attr('name', nameCity.replace(new RegExp("Cities\\[.*\\]", 'gi'), '[' + index + ']'));
}
var idCity = cityInput.attr('id');
if (idCity != null) {
cityInput.attr('id', idCity.replace(new RegExp("Cities_\\d+"), index));
}
});
});
});
This last bit probably requires some tweaking, as it's untested, but it's similar to something I've done before. You would call this whenever the items on your view are added/edited/removed/moved.
I came up with my own solution. It's a little bit of a hack, but I feel it's better than the alternatives. The other solution and suggestions all involved altering the markup and adding javascript to synchronize the added markup -- which I specifically said I did not want to do in the OP. I feel adding indexes to the <input /> names is redundant if said <input />s are already ordered in the DOM the way you want them. And adding javascript is just one more thing to maintain, and unnecessary bits sent through the wire.
Anyways .. My solution involves looping through the raw request body. I hadn't realized before that this is basically just a url-encoded querystring, and it's easy to work with after a simple url-decode:
public class StatesBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
controllerContext.HttpContext.Request.InputStream.Seek(0, SeekOrigin.Begin);
string urlEncodedFormData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
var decodedFormKeyValuePairs = urlEncodedFormData
.Split('&')
.Select(s => s.Split('='))
.Where(kv => kv.Length == 2 && !string.IsNullOrEmpty(kv[0]) && !string.IsNullOrEmpty(kv[1]))
.Select(kv => new { key = HttpUtility.UrlDecode(kv[0]), value = HttpUtility.UrlDecode(kv[1]) });
var states = new List<State>();
foreach (var kv in decodedFormKeyValuePairs)
{
if (kv.key == "stateName")
{
states.Add(new State { Name = kv.value, Cities = new List<City>() });
}
else if (kv.key == "cityName")
{
states.Last().Cities.Add(new City { Name = kv.value });
}
else if (kv.key == "cityPopulation")
{
states.Last().Cities.Last().Population = int.Parse(kv.value);
}
else
{
//key-value form field that can be ignored
}
}
return states;
}
}
This assumes that (1) the html elements are ordered on the DOM correctly, (2) are set in the POST request body in the same order, and (3) are received in the request stream on the server in the same order. To my understanding, and in my case, these are valid assumptions.
Again, this feels like a hack, and doesn't seem very MVC-y. But it works for me. If this happens to help someone else out there, cool.
Using VS2013, MVC5, assuming a model where there is a master/header record, and multiple related detail, the MVC view will display the header data in text boxes, and will display the detail in a web grid. The controller has an action method that accepts the id of the header record to display. There is no action method with a empty parameter list. When I click the resulting/displayed view's web grid header hyperlink I receive an error...
The parameters dictionary contains a null entry for parameter 'Id' of
non-nullable type 'System.Int32' for method
'System.Web.Mvc.ActionResult Index(Int32)' in
'WebGridTest.Controllers.HomeController'. An optional parameter must
be a reference type, a nullable type, or be declared as an optional
parameter. Parameter name: parameters
Here is my code...
Code
Models
MyDetail
namespace WebGridTest.Models
{
public class MyDetail
{
public string Column1 { get; set; }
public string Column2 { get; set; }
public string Column3 { get; set; }
}
}
MyHeader
using System.Collections.Generic;
namespace WebGridTest.Models
{
public class MyHeader
{
public MyHeader()
{
this.MyDetails = new List<MyDetail>();
}
public int Id { get; set; }
public List<MyDetail> MyDetails { get; set; }
}
}
Controllers
HomeController
using System.Web.Mvc;
namespace WebGridTest.Controllers
{
public class HomeController : Controller
{
public ActionResult Index(int Id)
{
var model = new Models.MyHeader();
model.Id = 2; // HARD CODED VALUES FOR DEMONSTRATING THIS ISSUE
var detail1 = new Models.MyDetail();
detail1.Column1 = "1A";
detail1.Column2 = "1B";
detail1.Column3 = "1C";
model.MyDetails.Add(detail1);
var detail2 = new Models.MyDetail();
detail2.Column1 = "2A";
detail2.Column2 = "2B";
detail2.Column3 = "2C";
model.MyDetails.Add(detail2);
var detail3 = new Models.MyDetail();
detail3.Column1 = "3A";
detail3.Column2 = "3B";
detail3.Column3 = "3C";
model.MyDetails.Add(detail3);
return View(viewName: "Index", model: model);
}
}
}
Views
Index.cshtml
#model WebGridTest.Models.MyHeader
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm(actionName: "Index", controllerName: "Home", method: FormMethod.Post))
{
<p></p>
#Html.TextBoxFor(x => x.Id)
<p></p>
WebGrid grid = new WebGrid(null, canPage: false);
grid.Bind(Model.MyDetails);
#grid.GetHtml(tableStyle: "table table-bordered lookup-table", columns:
grid.Columns(
grid.Column(
"Column1"
, "Column 1"
, (item) => (item.Column1)
)
,
grid.Column(
"Column2"
, "Column 2"
, (item) => (item.Column2)
)
,
grid.Column(
"Column3"
, "Column 3"
, (item) => (item.Column3)
)
)
)
}
when I run the project I receive the error described above. If I remove the parameter from action method 'Index' the program will not generate an error.
When I hover over the headers in the web page, I see the generated URL. I can see it does not include route values in the query string parameters list. It only includes sort values. Example...
http://localhost:62802/Home/Index?sort=Column1&sortdir=ASC
My example is contrived and trivial, but suffice to say, I need the Id to identify which records to display.
How can I specify the id route value on the column headers?
Upon research on the web, many developers suggest using JavaScript to modify the DOM to include the route values. That seems like it would work, but really moves away from MVC structures in favor of client-side evaluation and manipulation. The information is available in IIS/MVC, but the ability to apply the information (specifically to the web grid header anchor) is lacking.
I have found an alternative to JavaScript that keeps the solution entirely in C#. I will answer my own question as a Q&A style question.
The solution involves creating an extension method that will inject the route values into the resulting HTML prior to responding to the original client request. This entails adding the extension method class, and adding the method call in the view.
Example...
Extension Method Class
using System.Web;
namespace WebGridTest
{
public static class ExtensionMethods
{
public static IHtmlString AddRouteValuesToWebGridHeaders(this IHtmlString grid, string routeValues)
{
var regularString = grid.ToString();
regularString = regularString.Replace("?sort=", "?" + routeValues + "&sort=");
HtmlString htmlString = new HtmlString(regularString);
return htmlString;
}
}
}
Augmented View
(Notice the call to the extension method 'AddRouteValuesToWebGridHeaders')...
#model WebGridTest.Models.MyHeader
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm(actionName: "Index", controllerName: "Home", method: FormMethod.Post))
{
<p></p>
#Html.TextBoxFor(x => x.Id)
<p></p>
WebGrid grid = new WebGrid(null, canPage: false);
grid.Bind(Model.MyDetails);
#grid.GetHtml(tableStyle: "table table-bordered lookup-table", columns:
grid.Columns(
grid.Column(
"Column1"
, "Column 1"
, (item) => (item.Column1)
)
,
grid.Column(
"Column2"
, "Column 2"
, (item) => (item.Column2)
)
,
grid.Column(
"Column3"
, "Column 3"
, (item) => (item.Column3)
)
)
).AddRouteValuesToWebGridHeaders("Id=" + Model.Id.ToString())
}
Conclusion
Injecting into the HTML is not a preferred solution because like any other parsing algorithms it is highly assumptive. I have found no other way than HTML parsing. Perhaps downloading the Microsoft open-source version of this class and creating an override with the desired behavior would be a more elegant solution.
** Request ** - Microsoft - please add the ability to control/augment the anchor in the web grid header rendering.
Please feel free to comment...
At least 3 ways I can think of that this could be done:-
1st (ideal) - in a single telerik grid which has around 8 columns, 1st col would list all table entries with the next 6 for displaying different dates submitted for each entry but not all necessarily having a value for each entry, final col would link to each entry on a separate page to allow new dates to be submitted via datepicker or to be edited.
Main problem is I need to be able to display each of the dates on the grid in different colours depending on each col, by this I mean I record a date in 1st col of which has a yearly renewal so if >6months then it's colour 1, >1month colour 2, <1month colour 3 and finally if past 1 year mark then colour 4.
There are also 2 different possible renewal lengths for the other col's.
2nd - Each different renewal length would get its own grid so 1st for 1y, 2nd for 2nd length and 3rd for 3rd length.
3rd (likely) - 4 grids to replace the colours it would simply display each category so 1 grid would show all entries which had more than 6months, grid 2 would show greater than 1month, grid 3 would show less than 1month and grid 4 would show past time length.
I have no clue how best to sort the dates out in a way that would do what I need it to but I figure either option 1 will be possible or option 3 is the simplest.
Edit -
using System
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace (...).Models.DTO
{
public class ...DTO
{
public int Id { get; set; }
public string Name { get; set; }
//public string C1D
//{
// get
// {
// if (C1D < DateTime.Today.AddDays(-183)) return "Green";
// }
//}
public string C1D
{
get
{
if ((C1D = DateTime.ParseExact(C1D, "yyyy/mm/dd", null)) < DateTime.Today.AddDays(-183)) return "Green";
}
set;
}
public string C2D { get; set; }
Here it shows how I have tried to setup C1D in 2 different ways and for C2D how I usually setup the cols which go into the telerik grid.
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = new List<...DTO>();
_db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
This is how I would go about setting it up in the controller for displaying the data in the telerik grid.
Below is how I setup the view
<% Html.Telerik().Grid<(...).Models.DTO.(...)DTO>()
.Name("...List")
.DataKeys(dk => dk.Add(x => x.Id))
.Columns(c =>
{
c.Bound(x => x.Name);
c.Bound(x => x.C1D)
.Title("...");
c.Bound(x => x.C2D)
.Title("...");
c.Bound(x => x.C3D)
.Title("...");
c.Bound(x => x.C4D)
.Title("...");
c.Bound(x => x.C5D)
.Title("...");
c.Bound(x => x.C6D)
.Title("...");
c.Bound(x => x.C7D)
.Title("...");
})
.Sortable()
.Filterable()
.DataBinding(db => db.Ajax().Select("_List", "..."))
.Render();
%>
Edit 2 -
I've also tried
.ClientEvents(e => e.OnDataBound("onDataBound"))
function onDataBound(e) {
if (e.dataItem.C1D > DateTime.Today.AddDays(183)) {
e.cell.style.backgroundColor = "green";
}
if (e.dataItem.C1D > DateTime.Today.AddDays(30)) {
e.cell.style.backgroundColor = "orange";
}
if (e.dataItem.C1D > DateTime.Today) {
e.cell.style.backgroundColor = "red";
}
if (e.dataItem.C1D <= DateTime.Today) {
e.cell.style.backgroundColor = "purple";
}
}
and upon reaching this page it would break into code and say "Microsoft JScript runtime error: 'dataItem.C1D' is null or not an object" and "Microsoft JScript runtime error: 'cell.style' is null or not an object" and then display the page with all the dates in the grid so those items aren't null but is there otherwise some other code/format I should be using to perform this function?
And also looked at http://demos.telerik.com/aspnet-mvc/grid/customformatting in regards to .cellaction like below
.CellAction(cell =>
{
if (cell.Column.Title == "Title Name")
{
if (cell.DataItem.C1D > DateTime.Today.AddDays(183))
{
//Set the background of this cell only
cell.HtmlAttributes["style"] = "background:red;";
}
}
})
and I had to change .Name to .Title since it didn't recognise .Name, but I get the error msg "Error 1 Operator '>' cannot be applied to operands of type 'string' and 'System.DateTime' " so seems I won't be able to perform this complex a task in a cell action.
I've also posted this on the telerik forums attached to another question but so far no reply
http://www.telerik.com/community/forums/aspnet-mvc/grid/telerik-grid-row-custom-formatting-on-either-bit-int-string-field.aspx
Edit 3 -
Additional Controller code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using (Database Name).Models;
using (Database Name).Models.DTO;
using Telerik.Web.Mvc;
using Telerik.Web.Mvc.UI;
namespace (Database Name).Controllers
{
public class (Controller Name)Controller : Controller
{
(Database Name)Entities _db = new (Database Name)Entities();
public ActionResult List()
{
return View();
}
That's it now there's nothing left which I can possibly provide since there's nothing else which could have any kind of affect on the telerik grid so if there is still something else which might be hidden some place else that I may be missing then please explain what that might be since the only thing I haven't included is the code to do with the Create and Edit pages but all they involve is making each simple record then allowing the user to change the dates recorded.
Edit 3 :
When you do :
When you do _db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
}
}
x would be the ObjectFromDB, you don't want to assign each properties of the DTO, you want to pass the baseObject (witch is x), then return the value you want from x.
If you can provide me with youre solution using putfile or something else I can take a look at it if you want to but right now I don't know how it would be possible to help you more than that...
End Edit 3
Can you put some code?
I'll go with solution 1.
You could add a css class to a using ClientTemplate, if it's > [timespan], I think that you should add a colum that is bound on a property that could return name of a css class or an empty string depending on the time span. Let say you have a DateCol1 property witch is a DateTime you could add a DateCol1Css property that goes like this :
public string DateCol1Css
{
get
{
if(DateCol1 < DateTime.Now.AddMonths(-1)) return "Color1"; //witch is less than a month
if(DateCol1 < DateTime.Now.AddMonths(-3)) return "Color2"; //witch is less than 3 months
if(DateCol1 < DateTime.Now.AddMonths(-6)) return "Color3"; //witch is less than 6 months
return "";
}
}
public string DateCol2Css
{
get
{
if (DateCol2 < DateTime.Now.AddDays(-10)) return "Color1"; //witch is less than 10 days
if (DateCol2 < DateTime.Now.AddDays(-30)) return "Color2"; //witch is less than 30 days
return "";
}
}
public string DateCol3Css
{
get
{
if (DateCol3 < DateTime.Now.AddMonths(-1)) return "Color1"; //witch is less than a month
if (DateCol3 < DateTime.Now.AddMonths(-3)) return "Color2"; //witch is less than 3 months
if (DateCol3 < DateTime.Now.AddMonths(-6)) return "Color3"; //witch is less than 6 months
return "";
}
}
And the grid should be like this :
<%= Html.Telerik().Grid<SerializableAdmin>()
.Name("Grid")
.Columns(colums =>
{
colums.Bound(c => c.FirstName);
colums.Bound(c => c.Id);
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol1Css#>\"<#=DateCol1#></span>");
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol2Css#>\"<#=DateCol2#></span>");
colums.Bound(c => c.Id).ClientTemplate("<span class=\"<#=DateCol3Css#>\"<#=DateCol3#></span>");
})
%>
Edit :
Take a look at this code, you pass the object from the database to your new object and add property with get only on the db object.
public class ObjectDTO
{
public ObjectFromDB BaseObject { get; set; }
public int Id
{
get { return BaseObject.Id; }
}
public string Name
{
get { return BaseObject.Name; }
}
public string C1D
{
get
{
if (BaseObject.C1SC.HasValue && BaseObject.C1SC < DateTime.Now.AddDays(-183)) return "Green";
return string.Empty;
}
}
public string C2D
{
get
{
if (BaseObject.C2SC.HasValue && BaseObject.C2SC < DateTime.Now.AddDays(-183)) return "Green";
return string.Empty;
}
}
}
[GridAction]
public ActionResult _List(int? Id)
{
List<ObjectDTO> ret = new List<ObjectDTO>();
_db.GetObjectFromDB().ToList().ForEach(x =>
{
ret.Add(new ObjectDTO { ObjectFromDB = x } );
});
}
For this code block, have you try to cast the string in datetime?
.CellAction(cell =>
{
if (cell.Column.Title == "Title Name")
{
if (!string.IsNullOrEmpty(cell.DataItem.C1D) && DateTime.ParseExact(cell.DataItem.C1D, "yyyy/mm/dd", null) > DateTime.Today.AddDays(183))
{
//Set the background of this cell only
cell.HtmlAttributes["style"] = "background:red;";
}
}
})
And your ...Dto property for the CssColor should be like this :
public class ...DTO
{
public int Id { get; set; }
public string Name { get; set; }
public string C1D
{
get
{
if (!C1SD.HasValue) return string.Empty;
return (DateTime.ParseExact(C1SD, "yyyy/mm/dd", null) < DateTime.Today.AddDays(-183)) ? "Green" : "";
}
}
}
So your GridAction would be like this :
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = _db.(...).ToList();
...
Let me know if it helps!
This was what I tried to do from the start
[GridAction]
public ActionResult _List(int? Id)
{
List<...DTO> ret = new List<...DTO>();
_db.(...).ToList().ForEach(x =>
{
ret.Add(new ...DTO
{
Id = x.Id,
Name = x.(...)Name,
C1D = (x.C1SD.HasValue) ? x.C1SD.Value.ToShortDateString() : "",
C2D = (x.C2SD.HasValue) ? x.C2SD.Value.ToShortDateString() : "",
Trick is to do all the calculations in the controller and leave the model and view very basic.
Model- left em all as string and basically did public string blah { get; set;} for each date col and then for each col you want to do something complex like my date calculations you would make an additional col, this would be for the colour/whatever feature you want heck you could even setup an admin function so if they don't have win auth or aren't in correct role etc then it would splarf the data or de-link a url link.
Controller- well as you can see above thats how I sorted out the dates showing up and now the surprisingly simple way to sort out the colour or w/e, (example thing blahDTO bdt = new blahDTO(); )
if (x.TestVal1 != null)
{
if ((x.TestVal1) > (DateTime.Today.AddMonths(6)))
{
bdt.Colourflag1 = "green";
}
Now it doesn't have to be green, it could be true false tom dick or jane w/e but it would just have to be a value assigned based on certain unique conditions.
View- when I realised it could be this easy I facepalmed myself, anyway yeh so c.Bound(x => x.Colourflag1).Hidden(true); next step
.ClientEvents(events => events.OnRowDataBound("onRowDataBound"))
<script type="text/javascript">
function onRowDataBound(e) {
if (e.dataItem.TestVal1 == "green") {
e.row.cells[1].style.backgroundColor = "green";
}
and hey presto you just turn the 1st row/col cell green and this can be twisted and used into w/e e.row.cell[?]. can be used for and you have a single cell do all magic ye ha rolled into 1.
Now I know my jscript code is wasteful since I'm sure at this point you could make the green be an object which would then affect the the next object so if yellow then it makes the background colour code fit in yellow.
If anybody has any questions or jscript advice feel free to ask/comment.
In my viewData I have an IList mls.
I want to use this to show in a dropdown. Like so:
<%= Html.DropDownList("ml3Code",
new SelectList(Model.Mls, "Code", "Description", Model.Ml3.Code ?? ""),
Model.T9n.TranslateById("Labels.All"),
new { #class = "searchInput" })%>
This works fine, until there's a myObject.Code == VOC<420 g/l.
I would have expected that an HTML helper would encode its values, but it doesn't.
How should I approach this problem? The only thing I can come up with is first making a dupe list of the objects with encoded values and then feeding it to the selectlist. This would be really bothersome.
P.S. I hope Phill H. and his team will have a long and thorough look at the encoding for asp.net-mvc 2.0...
I'm puzzled. The question "Do ASP.NET MVC helper methods like Html.DropDownList() encode the output HTML?" was asked on SO before, and the answer was "Yes" - and the source-code from the MVC framework was cited to back this assertion up.
Well, you can roll your own Html helper, but if you're like me you won't want to do that.
To me, I see two options here:
Write your select element in plain view without the helper. I've never felt the helpers provide you much save for highlighting an element when an error occurs.
Patch the select box on the client when the page loads, as in:
function encodeHtml(str)
{
var encodedHtml = escape(str);
encodedHtml = encodedHtml.replace(///g,"%2F");
encodedHtml = encodedHtml.replace(/\?/g,"%3F");
encodedHtml = encodedHtml.replace(/=/g,"%3D");
encodedHtml = encodedHtml.replace(/&/g,"%26");
encodedHtml = encodedHtml.replace(/#/g,"%40");
return encodedHtml;
}
window.onload = function()
{
var ml3Code = document.getElementById("ml3Code");
for(var i = 0; i < ml3Code.options.length; ++i)
{
ml3Code.options[i].value = encodeHtml(ml3Code.options[i].value);
}
};
It's a hack, I know. I strongly prefer the first choice.
This is encoded. But dont check with firebug - It shows values decoded.
Check in ViewSource of the Browser and things are encoded.
Controller
public List<CategoryInfo> GetCategoryList()
{
List<CategoryInfo> categories = new List<CategoryInfo>();
categories.Add(new CategoryInfo { Name = "Food<äü", Key = "VOC<420 g/l", ID = 2, Uid = new Guid("C0FD4706-4D06-4A0F-BC69-1FD0FA743B07") });
}
public ActionResult Category(ProductViewModel model )
{
IEnumerable<SelectListItem> categoryList =
from category in GetCategoryList()
select new SelectListItem
{
Text = category.Name,
Value = category.Key
};
model.CategoryList = categoryList;
return View(model);
}
View
<%= Html.DropDownList("Category" , Model.CategoryList) %>
Model
public class ProductViewModel
{
public IEnumerable<SelectListItem> CategoryList { get; set; }
public List<CategoryInfo> Categories { get; set; }
}
HTML
<select id="Category" name="Category"><option value="VOC<420 g/l">Food<äü</option>
</select>