I'm using MVC's WebGrid throughout my application and it's been working great. I'm setting the columns for the grid in my controller code and then passing that to my view. I add a new column like this where columns is a List<MvcWebGridColumn>:
columns.Add(new WebGridColumn { ColumnName = "Path", Header = "Full Path", Filter = ... });
And then in my view, I set the grid columns from my view model like this:
#grid.GetHtml(
footerStyle: "hide",
mode: WebGridPagerModes.All,
htmlAttributes: new { id = #Model.Id },
columns: #Model.Columns)
Up until this point, setting the columns in the code has worked great as the columns have been known, first-level properties of my models. Now things are more complicated and I don't know how to handle it with the WebGrid. Here is an example model:
public class Requirement
{
public string Id { get; set; }
public string Path { get; set; }
public Dictionary<string, string> Fields { get; set; }
}
I want to be able to set a WebGrid's column in the code using the value from the Field dictionary property's key. I'm trying this, but it's not working:
columns.Add(new WebGridColumn { ColumnName = String.Format("Fields[\"{0}\"]", key), Header = label });
I tried also putting a method GetFieldValue(string key) method on my Requirement model, but it doesn't like that either. It seems like it can only handle exactly matching an object's property identifier.
I'm not sure if this is possible using WebGrid out of the box or if I have to extend the functionality. Thanks in advance.
I believe I just came up with a way to handle this that doesn't seem too hacky.
To restate the problem, I believe the MVC WebGrid uses reflection to set the ColumnName property so the string you enter for that needs to be directly accessible via the object's API eg. "Id", "Field.Address.Name", etc. The problem in my case is that the value I need for my column's row comes from a Dictionary and it doesn't seem like you can pass a parameter (key, index, etc) into the ColumnName to get the value I need.
My solution is to avoid using the ColumnName property of the WebGrid entirely when resolving the value to be displayed. I simply enter a value for the grid's model that I know exists for the ColumnName, in my case "Id". This is just used so the WebGrid will render. Then, since the Format of the WebGridColumn uses a dynamic function, I can define my column value using whatever I want:
columns.Add(new WebGridColumn
{
ColumnName = "Id",
Header = field.Label,
Format = x => new HtmlString(x.Value.GetEntityFieldValue(field.Field))
});
I soon realized that this will mess up sorting. MVC WebGrid's black-box sort simply uses the ColumnName in the sort query so every column for me now has "?sort=Id". If you're not sorting, then you're OK. If you are, you'll have to implement a custom sorting solution like I'm currently doing.
Related
I have a DataTable (DataTable) made of 3 columns: Time(double), Values(double), Digitals(DigitalModel)
DataTable is displayed in a DataGrid where columns are generated automatically and the last one is being intercepted during creation and a specific data template been applied to correctly display the DigitalModel type.
I'm using Caliburn Micro and Mahapps libraries for UI
The Digital Model class is defined as:
public class DigitalModel : PropertyChangedBase{
public DigitalModel(){
SourceDigital = new BindableCollection<string>();
SelectedDigital = new BindableCollection<string>();
}
public BindableCollection<string> SourceDigital{ get; set; }
public BindableCollection<string> SelectedDigital{ get; set; }
}
I need to be able to sort the DataTable through the time column. So I use DataTable.DefaultView.Sort = "Time"
For testing purpose, only one row is sorted at a time, using the CellEditEndingevent and sorting when a "Time" cell has been updated.
(I'm going incrementally here instead of using the sorting functionality of the Mahapps DataGrid Control.
It does work correctly for the Time value and Values value. They are being correctly recopied to the new table position but the last column some of the data is being lost...
So here is the behavior when I sort it (from a view perspective)
Before:
SourceDigital = {"one", "two", "three'}
SelectedDigital = {"two"}
After sorting I have:
SourceDigital = {"one", "two", "three'}
SelectedDigital = {}
I debugged through the CellEditEnding event and also through the CurrentCellChanged, to see the before and after, and it seems like the sort method keeps all the data correctly. In between CellEditEnding and CurrentCellChanged something is messing my third column and clearing one of the collection.
It seems to be a UI issue ? But through the debugging in the ViewModel, I can see that the DataRowView being passed by the CurrentCellChanged event, doesn't the correct object in the third column....
So confused....
Thank you for your help
I am trying to set a value for DataGridViewRow.Tag when data binding but I don't know how to do it?
I tried with DataRow row = table.NewRow(); But row doesn't have tag.
How to set a value for DataGridViewRow.Tag when binding DataGridView to table (for example)? Or it isn't possible?
Edit1:
Here is the code i am using:
var table = new DataTable();
table.Columns.Add("Title", typeof(string));
table.Columns.Add("URL", typeof(string));
table.Columns.Add("Read Later", typeof(bool));
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
var id = node.Attribute("id").Value;
var url = node.Element("path").Value;
var comment = node.Element("title").Value;
var readlater = node.Attribute("readLater")?.Value.ToString() == "1";
var row = table.NewRow();
row.ItemArray = new object[] { url, comment, readlater };
table.Rows.Add(row);//Edit2
}
dataGridView1.DataSource = table;
I am trying to set a tag for the row to use it in CellClick event:
var cRow = dataGridView1.Rows[e.RowIndex];
var id = cRow.Tag.ToString();
Separate the data from how it is displayed
When using a DataGridView, it is seldom a good idea to access the cells and the columns directly. It is way more easier to use DataGridView.DataSource.
In modern programming there is a tendency to separate your data (= model) from the way your data is displayed (= view). To glue these two items together an adapter class is needed, which is usually called the viewmodel. Abbreviated these three items are called MVVM.
Use DataGridView.DataSource to display the data
Apparently, if the operator clicks a cell, you want to read the value of the tag of the row of the cell, to get some extra information, some Id.
This displayed row is the display of some data. Apparently part of the functionality of this data is access to this Id. You should not put this information in the view, you should put it in the model.
class MyWebPage // TODO: invent proper identifier
{
public int Id {get; set;}
public string Title {get; set;}
public string Url {get; set;}
public bool ReadLater {get; set;}
... // other properties
}
Apparently you have a method to fetch the data that you want to display from a sequence of nodes. Separate fetching this data (=model) from displaying it (= view):
IEnumerable<MyWebPage> FetchWebPages(...)
{
...
foreach (XElement node in nodes)
{
Helper.CheckNode(node);
bool readLater = this.CreateReadLater(node);
yield return new MyWebPage
{
Id = node.Attribute("id").Value,
Url = node.Element("path").Value,
Title = node.Element("title").Value,
ReadLater = this.CreateReadLater(node),
};
}
}
I don't know what is in node "ReadLater", apparently you know how to convert it to a Boolean.
bool CreateReadLater(XElement node)
{
// TODO: implement, if null return true; if not null ...
// out of scope of this question
}
For every property that you want to display you create a DataGridViewColumn. Property DataPropertyName defines which property should be shown in the column. Use DefaultCellStyle if a standard ToString is not enough to display the value properly, for instance, to define the number of digits after the decimal point, or to color negative values red.
You can do this using the visual studio designer, or you can do this in the constructor:
public MyForm
{
InitializeComponents();
this.dataGridViewColumnTitle.DataPropertyName = nameof(MyWebPage.Title);
this.dataGridViewColumnUrl.DataPropertyName = nameof(MyWebPage.Url);
...
}
You don't want to display the Id, so there is no column for this.
Now to display the data, all you have to do is assign the list to the datasource:
this.dataGrieViewWebPages.DataSource = this.FetchWebPages().ToList();
This is display only. If the operator can change the displayed values, and you want to access the changed values, you should put the items in an object that implements interface IBindingList, for instance, using class (surprise!) BindingList<T>:
private BindingList<MyWebPage> DisplayedWebPages
{
get => (BindingList<MyWebPage>)this.dataGrieViewWebPages.DataSource;
set => this.dataGrieViewWebPages.DataSource = value;
}
Initialization:
private void DisplayWebPages()
{
this.DisplayedWebPages = new BindingList<MyWebPage>(this.FetchWebPages.ToList());
}
And presto! All webpages are displayed. Every change that the operator makes: add / remove / edit rows are automatically updated in the DisplayedWebPages.
If you want to access the currently selected WebPages:
private MyWebPage CurrentWebPage =>(MyWebPage)this.dataGrieViewWebPages.CurrentRow?.DataBoundItem;
private IEnumerable<MyWebPage> SelectedWebPages =>
this.dataGrieViewWebPages.SelectedRows
.Cast<DataGridViewRow>()
.Select(row => row.DataBoundItem)
.Cast<MyWebPage>();
Now apparently whenever the operator clicks a cell, you want to do something with the Id of the WebPage that is displayed in the Row of the cell.
View: Displayed Cell and Row
ViewModel: React when operator clicks a cell
Model Action that must be done
React on Cell Click: get the Id
We've handled the View above. ViewModel is the event handler:
void OnDatGridViewCellClicked(object sender, DataGridViewCellEventArgs e)
{
// use the eventArgs to fetch the row, and thus the WebPage:
MyWebPage webPage = (MyWebPage)this.dataGridViewWebPages.Rows[e.RowIndow].DataBoundItem;
this.ProcessWebPage(webPage);
}
ProcessWebPage is typically a method in your Model class:
public void ProcessWebPage(MyWebPage webPage)
{
// Do what you need to do if the operator clicks the cell, for example:
int webPageId = webPage.Id;
...
}
Conclusion: advantages of separating model from view
By the way, did you see that all ViewModel methods are one-liners? Only your Model methods FetchWebPages and ProcessWebPage contain several lines.
Because you separated the View from the Model, changes to your Model or your View will be fairly simple:
If you want to store your data in Json format, or in a database instead of in an XML, your View won't change
If you don't want to react on cell click, but on Button OK click, then your Model won't change. Your model also doesn't have to change if you decide to show more or less columns
Because you separated your Model from your View, the Model can be unit tested without a form. You can also test the View with a Model filled with only test values.
I'm autogenerating columns based on a set. After that, I'm adding one of my own, manually, like this.
private void OnAutoGeneratedColumns(Object sender, EventArgs eventArgs)
{
DataGridColumn column = new DataGridTextColumn { ... };
DataGrid grid = sender.Get<DataGrid>();
grid.Columns.Add(column);
}
It adds the column and I can see the header being set according to the code. However, I'm not sure how to add any data to it. Checking the list of properties didn't offer me a clue. How should one do that?
The additional column is to be rendered based on the information from the same line in the grid (e.g. if the autogenerated columns are FirstName and LastName, the manually added one would be FullName and consist of the concatenation of them).
One approach I can think of is to inherit the class that is being used to autogenerate the columns, extend it with a new property and put it in the constructor of my view model like so. Then I could bind to the latter instead of the former.
BasicView = new ListCollectionView(basicThings);
ExtendedView = new ListCollectionView(extendedThings);
class ExtendedThing : BasicThing
{
public String FullName { get { return FirstName + LastName; } }
}
But that seems like a rather major intrusion into the code, which makes me suspect I'm heading in the wrong direction...
I am wondering if there is another way to express the following bit of code. The code works as expected, but I have an issue with how it defines my naming conventions.
//Model.cs:
[DisplayName("Device Manufacturer")]
public List<KeyValuePair<int, string>> DeviceManufacturer { get; set; }
public int SelectedManufacturerID { get; set; }
//Model.ascx:
<%= Html.DropDownListFor(model => model.SelectedManufacturerID, new SelectList(Model.DeviceManufacturer, "Key", "Value"))%>
Now, whenever the selected list item is changed -- the new value is stored in SelectedManufacturerID. I am able to receive the value in my controller like so:
int selectedID = model.SelectedDataCenterID;
However, I am unhappy with the fact that MVC gives my select list the ID 'SelectedManufacturerID' when it is not an ID field -- it is a select list. Clearly this is occurring to support the binding to the SelectedManufacturerID field.
If I wish to work with my select list client-side, I now have code such as:
$('#SelectedManufacturerID').change(function(){
console.log("OnSelectedIndexChanged!");
});
This jQuery is very unclear. There is no indication that SelectedManufacturerID is a select DOM element. It seems impossible to have proper naming conventions using the MVC conventions at the top of this post. Does anyone else feel this way? Do I have other options?
If you want,you can update the ID or other html attributes of the dropdown by using a different overload of DropDownListFor helper method. Here you can pass html attributes as the last parameter.
<%= Html.DropDownListFor(model => model.SelectedManufacturerID,
new SelectList(Model.DeviceManufacturer, "Key", "Value"),
new { #id="ManufacturerList"})%>
Use select#SelectedManufacturerID instead.
I'm trying to use a WebGrid to display data in my model and am having a huge number of issues. My model contains among other things this:
public IEnumerable<Auction> Auctions { get; set; }
What I have done is:
#{
var grid = new WebGrid(Model.Auctions, rowsPerPage: Model.PagingInfo.ItemsPerPage, defaultSort: "Title", canPage: true, canSort: true)
{SortDirection = SortDirection.Ascending};
grid.Pager(WebGridPagerModes.NextPrevious);
}
I want to display some text in the first column depending on the type of the auction in the current row, so I have written a method in the model:
public string GetAuctionType(Auction auction)
{
var type = string.Empty;
if (auction is LubAuction)
{
type = "Lowest unique wins";
}
else if (auction is EsfAuction)
{
type = "Highest wins";
}
return type;
}
Now, my view also contains:
#grid.GetHtml(
columns: grid.Columns(
grid.Column("OwnerReference", header: "Owner reference")
)
);
Question is how do I add a grid.Columns line in the above to display the text in GetAuctionType?
Also, the other issue is that there is no pager appearing and sorting does not work.
I would appreciate all help.
Thanks,
Sachin
I would move GetAuctionType logic to a Partial Class so you can access it like a normal property on each object in your collection. You may also want to take a look at question ASP.NET MVC3 WebGrid format: parameter that is covering usage of WebGrid's column format syntax.
Regarding your other issues, do you see any errors in javascript console ?