My grid is bound to a List(Of ), not a DataSet.
I managed to get the add to work by adding a new item to the collection and rebinding the grid.
However, whenever the user adds another item, the rows are reset and the changes in the edited rows are lost.
I created a sample solution that attempts to demonstrate this behavior, but there is an error in the javascript when you click the Add Thing button.
The code is here: http://tinyurl.com/6yq7zc
When you run it, the collection of Things is loaded with 10 Thing objects. This is bound to the grid. You can click the edit button in a row to toggle that row into edit mode. I want to be able to click the Add Thing button in the Command Bar to Add one or more new Things with each being in Edit mode so the user can add a bunch of Thing objects and then fill in the details for the set of them.
Thanks for your help.
16 Answers, 1 is accepted
The setup which you described is already being addressed in the support ticket opened on the matter. To avoid duplicate posts, we can continue our communication there.
Sincerely yours,
Yavor
the Telerik team
Check out Telerik Trainer, the state of the art learning tool for Telerik products.
Please help!
Thanks,
Graeme
The only solution in this case is, whenever a new command is raised, to preserve the already edited items in the database. The following topic shows how to iterate through all the items in the control, and get the values. Then, these can be updated in the underlying database.
I hope this helps.
Kind regards,
Yavor
the Telerik team
Check out Telerik Trainer, the state of the art learning tool for Telerik products.
First, for anyone else who reads this thread, I am binding my grid to a Collection, not a DataSet. So I have an in-memory representation of my data that will not automatically write changes back to the database until I tell it to SaveChanges().
Summary: I need to be able to Add/Edit multiple rows in a Grid without losing the changes made to other Added/Edited rows that have not yet been saved.
Solution: Before any Add or Edit operation, I need to scrape the values off the edit row controls into the relevant object in the Collection data source.
Here is the code in the ItemCommand event handler where I Add a new item:
Private Sub RadGrid1_ItemCommand(ByVal source As Object, ByVal e As Telerik.Web.UI.GridCommandEventArgs) Handles RadGrid1.ItemCommand |
If e.CommandName = "AddNewThing" Then |
ScrapeAllEditRows() |
Dim aThing As New Thing |
aThing.Guid = Guid.NewGuid() |
ThingController.Things.Add(aThing) |
_newThing = aThing |
RadGrid1.Rebind() |
End If |
End Sub |
And here is the code in the EditCommand event handler:
Private Sub RadGrid1_EditCommand(ByVal source As Object, ByVal e As Telerik.Web.UI.GridCommandEventArgs) Handles RadGrid1.EditCommand |
ScrapeAllEditRows() |
End Sub |
Lastly, the ScrapeAllEditRows() method loops through the rows like this:
Private Sub ScrapeAllEditRows() |
For Each item As GridDataItem In RadGrid1.MasterTableView.Items |
If item.IsInEditMode Then |
'get the key out of the row |
Dim key As Guid |
key = item.OwnerTableView.DataKeyValues(item.ItemIndex)("Guid") |
'get the thing out of the collection for the key |
Dim thing As Thing = ThingController.Things.FindByGuid(key) |
thing.Name = DirectCast(item("NameColumn").Controls(0), TextBox).Text |
thing.ShapeType = DirectCast(item.FindControl("RadComboBox1ShapeType"), RadComboBox).SelectedValue |
End If |
Next |
End Sub |
I.e. Create a grid with multi-edit turned on. Toggle a row into edit and make a change, now toggle another row into edit and see how the first change is lost. Right.
The ScrapeAllRows is one way (so far the only way I know of) to work around this. Essentially this is caching the changes made to existing rows in the collection the grid is bound to, without saving the changes to the database, and then rewriting them back to the grid when it re-renders. If the grid were to be Refreshed or the edit Canceled, I force a reload of the collection or object so the cached changes are not persisted to the database.
In the Grid, add a button in the CommandItemTemplate that calls your custom Add command. (AddNewThing in this example).
<CommandItemTemplate> |
<table width="100%"> |
<tr> |
<td> |
<asp:LinkButton ID="LinkButton2" runat="server" CommandName="AddNewThing" Text="Add Thing" /> |
</td> |
</tr> |
</table> |
</CommandItemTemplate> |
In the code behind, you need to fake out the Add. We're actually going to add the item to the collection, and save it to the database, and then reload the grid with that item in edit mode, so we actually skip the whole "ItemInserted" event. The grid does some strange binding things for inserted rows, and will only allow one row in insert mode at a time. This code works around that.
Remember, I am binding my grid to a Collection, NOT a DataSet, and I am NOT using a Object/SQL/Etc.DataSource of any sort. All the binding is handled in the code.
Private Sub RadGrid1_NeedDataSource(ByVal source As Object, ByVal e As Telerik.Web.UI.GridNeedDataSourceEventArgs) Handles RadGrid1.NeedDataSource |
Me.RadGrid1.DataSource = ThingController.Things |
End Sub |
So, first, we need to handle the AddNewThing command, like this:
Private Sub RadGrid1_ItemCommand(ByVal source As Object, ByVal e As Telerik.Web.UI.GridCommandEventArgs) Handles RadGrid1.ItemCommand |
If e.CommandName = "AddNewThing" Then |
ScrapeAllEditRows() |
Dim aThing As New Thing |
aThing.Guid = Guid.NewGuid() |
aThing.EstimatedBirthMonth = Date.Now.ToLongTimeString |
ThingController.Things.Add(aThing) |
_newThing = aThing |
RadGrid1.Rebind() |
End If |
End Sub |
Next, notice how we set the class variable _newThing equal to the new aThing that we just created? When the Grid rebinds, we can catch the PreRender event and check to see if there is a Thing in the _newThing variable, if there is, then we must have just added one, and we need to make it editable and Rebind the grid again (Yes, rebind again, it's weird that way.), like this:
Private Sub RadGrid1_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles RadGrid1.PreRender |
If _newThing IsNot Nothing Then |
For Each item As GridDataItem In RadGrid1.MasterTableView.Items |
Dim aThing As CodeForTelerik.Thing = DirectCast(item.DataItem, Thing) |
If aThing.Guid.Equals(_newThing.Guid) Then |
If Not item.IsInEditMode Then |
'clear the newdeal reference and set the row into edit mode |
_newThing = Nothing |
item.Edit = True |
RadGrid1.MasterTableView.Rebind() |
Exit Sub |
End If |
End If |
Next |
End If |
End Sub |
As with the ScrapeAllRows code, we are messing with the in-memory collection, so we are not persisting any of these changes into the database until the user tells us to. If the user Cancels or Refreshes, the collection/object is reloaded from the database and the new item is discarded.
I hope that helps. Please post to this thread if you have any questions or if you have a better suggestion (please, I wish I knew a better way to do this).
Simon!
Great solution! And you inspired me to another (not-exactly-fully-blown) solution.
We can use their ExtractValuesFromItem to produce a Hashtable which we save for ItemDataBound. Then, if there is an error, override the databinding with the value from the Hashtable. Like this [This uses Northwind.mdb.]:
ASPX: multi-row edit Grid; external "Update" button:
<radx:RadGrid ID="RadGrid1" runat="server" AutoGenerateColumns="False" GridLines="None" | |
OnPreRender="RadGrid1_PreRender" AllowMultiRowEdit="True" | |
OnNeedDataSource="RadGrid1_NeedDataSource" OnItemDataBound="RadGrid1_ItemDataBound"> | |
<MasterTableView DataKeyNames="CustomerID" EditMode="inPlace"> | |
<RowIndicatorColumn> | |
<HeaderStyle Width="20px" /> | |
</RowIndicatorColumn> | |
<ExpandCollapseColumn> | |
<HeaderStyle Width="20px" /> | |
</ExpandCollapseColumn> | |
<Columns> | |
<radx:GridBoundColumn DataField="CustomerID" HeaderText="CustomerID" ReadOnly="True" | |
SortExpression="CustomerID" UniqueName="CustomerID"> | |
</radx:GridBoundColumn> | |
<radx:GridBoundColumn DataField="ContactName" HeaderText="ContactName" SortExpression="ContactName" | |
UniqueName="ContactName"> | |
</radx:GridBoundColumn> | |
<radx:GridBoundColumn DataField="CompanyName" HeaderText="CompanyName" SortExpression="CompanyName" | |
UniqueName="CompanyName"> | |
</radx:GridBoundColumn> | |
</Columns> | |
</MasterTableView> | |
<ClientSettings> | |
<Selecting AllowRowSelect="True" /> | |
</ClientSettings> | |
<FilterMenu EnableTheming="True"> | |
<CollapseAnimation Duration="200" Type="OutQuint" /> | |
</FilterMenu> | |
</radx:RadGrid> | |
<asp:Literal ID="ErrorsLiteral" runat="server"></asp:Literal> | |
<asp:Button ID="Button1" runat="server" CommandName="Update" OnCommand="Button_Command" | |
Text="Update" /> | |
Code: Button_Command which throws an error (so we can see if the edit values are preserved) and Item_DataBound handler which compares/replaces the bound values with edit values:
protected void Button_Command(object sender, CommandEventArgs e) | |
{ | |
switch (e.CommandName) | |
{ | |
case "Update": | |
try | |
{ | |
CacheEditValues(); | |
foreach (GridEditableItem editItem in this.RadGrid1.EditItems) | |
{ | |
//Throw out an error to interfere with the save. Will edit values be preserved? | |
throw new InvalidOperationException("this is a bad thing."); | |
} | |
isError = false; | |
} | |
catch (InvalidOperationException ix) | |
{ | |
isError = true; | |
List<string> errorList = new List<string>(); | |
errorList.Add(ix.Message); | |
DisplayErrors(errorList); | |
} | |
break; | |
} | |
} | |
private bool isError = false; | |
private List<Hashtable> hashList = new List<Hashtable>(); | |
private void CacheEditValues() | |
{ | |
foreach (GridEditableItem editItem in this.RadGrid1.EditItems) | |
{ | |
Hashtable newValues = new Hashtable(); | |
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); | |
hashList.Add(newValues); | |
} | |
} | |
protected void RadGrid1_PreRender(object sender, EventArgs e) | |
{ | |
//Show all the rows as editable. | |
foreach (GridDataItem dataItem in this.RadGrid1.Items) | |
{ | |
dataItem.Edit = true; | |
} | |
this.RadGrid1.Rebind(); | |
} | |
protected void RadGrid1_NeedDataSource(object source, GridNeedDataSourceEventArgs e) | |
{ | |
string query = "SELECT TOP 10 [CustomerID], [ContactName], [CompanyName] FROM [Customers]"; | |
this.RadGrid1.DataSource = DataSourceHelper.GetDataTable(query); | |
} | |
protected void RadGrid1_ItemDataBound(object sender, GridItemEventArgs e) | |
{ | |
if (isError && e.Item.Edit && e.Item.RowIndex > -1 && e.Item.RowIndex < hashList.Count) | |
{ | |
GridEditableItem editItem = e.Item as GridEditableItem; | |
//put things back | |
Hashtable ht = hashList[e.Item.RowIndex]; | |
foreach (DictionaryEntry de in ht) | |
{ | |
Control c = editItem[de.Key.ToString()].Controls[0]; | |
switch (c.GetType().Name) | |
{ | |
case "TextBox": | |
TextBox tb = (c as TextBox); | |
if (tb.Text != de.Value.ToString()) | |
{ | |
(c as TextBox).Text = de.Value.ToString(); | |
} | |
break; | |
} | |
} | |
} | |
} | |
private void DisplayErrors(List<String> errorList) | |
{ | |
StringBuilder sb = new StringBuilder(); | |
sb.Append("<ul style=\"color:red;\">"); | |
for (int i = 0; i < errorList.Count; i++) | |
{ | |
string s = errorList[i]; | |
sb.Append("<li>"); | |
sb.Append(s); | |
sb.Append("</li>"); | |
} | |
sb.Append("</ul>"); | |
this.ErrorsLiteral.Text = sb.ToString(); | |
} | |
private void ClearErrors() | |
{ | |
this.ErrorsLiteral.Text = ""; | |
} | |
In my production environment, I also use a typed DataCollection, which can make it even easier to compare values (e.g., DirectCast( editItem.DataItem, Thing)). Sorry if my VB is faulty. :-)
Also, I didn't do the row control like you did, nor did I test for other control types, but I didn't want to muck up my keen-o example with a lot of annoying details. ;-)
Thanks very much, Simon. Let me know what you think, if you have a moment (even if you have to tactfully bring me down to earth).
Graeme
Graeme
private void CacheEditValues() | |
{ | |
foreach (GridEditableItem editItem in this.RadGrid1.EditItems) | |
{ | |
Hashtable newValues = new Hashtable(); | |
newValues.Add("CustomerID", editItem.OwnerTableView.DataKeyValues[editItem.ItemIndex]["CustomerID"]); | |
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); | |
hashList.Add(newValues); | |
} | |
} | |
...and then look up the particular HT later, by the PK.
Graeme
I figured out another way to do this via a collection. If there is an error (or whatever your flag is--isInserting, e.g.), you can modify the collection using the cached values during the NeedDataSource event handler before you assign it to the DataSource of the Grid. So if you wanted to do an "AddNew," or whatever, you could add an empty/default object to the Collection before binding. Check it out (remember isError is my flag to say we are rebinding due to an error during validation):
OrderDetailViews views = new OrderDetailViews(); | |
views.Sort("ID", System.ComponentModel.ListSortDirection.Ascending); | |
if (isError) | |
{ | |
foreach (OrderDetailView view in views) | |
{ | |
//put things back | |
Hashtable ht = hashList[view.ID] as Hashtable; //hashList[e.Item.ItemIndex]; | |
DateTime? selectedDate; | |
if (view.IsOrdered != Convert.ToBoolean(ht["IsOrdered"])) { view.IsOrdered = Convert.ToBoolean(ht["IsOrdered"]); } | |
if (view.Quantity != Convert.ToInt32(ht["Quantity"])) { view.Quantity = Convert.ToInt32(ht["Quantity"]); } | |
if (view.CancelQuantity != Convert.ToInt32(ht["CancelQuantity"])) { view.CancelQuantity = Convert.ToInt32(ht["CancelQuantity"]); } | |
if (view.ReceiveQuantity != Convert.ToInt32(ht["ReceiveQuantity"])) { view.ReceiveQuantity = Convert.ToInt32(ht["ReceiveQuantity"]); } | |
if (view.BackOrderQuantity != Convert.ToInt32(ht["BackOrderQuantity"])) { view.BackOrderQuantity = Convert.ToInt32(ht["BackOrderQuantity"]); } | |
if (view.UnitPrice != Convert.ToDecimal(ht["UnitPrice"])) { view.UnitPrice = Convert.ToDecimal(ht["UnitPrice"]); } | |
if (view.Comment != ht["Comment"].ToString()) { view.Comment = ht["Comment"].ToString(); } | |
selectedDate = (DateTime?)ht["BackOrderETADate"]; | |
if (selectedDate.HasValue && view.BackOrderETADate != selectedDate.Value) | |
{ | |
view.BackOrderETADate = selectedDate.Value; | |
} | |
else | |
{ | |
view.BackOrderETADate = DateTimeHelper.EmptyDate; | |
} | |
} | |
} | |
this.OrderDetailRadGrid.DataSource = views; | |
I forgot to mention that I changed the List<Hashtable> to a ListDictionary (so that I could have key access to the items):
private bool isError = false; | |
private ListDictionary hashList = new ListDictionary(); | |
private void CacheEditValues() | |
{ | |
foreach (GridEditableItem editItem in this.OrderDetailRadGrid.EditItems) | |
{ | |
Hashtable newValues = new Hashtable(); | |
object key = editItem.OwnerTableView.DataKeyValues[editItem.ItemIndex]["ID"]; | |
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); | |
hashList.Add(key, newValues); | |
} | |
} | |
Graeme
Most of my columns use ItemTemplates and EditTemplates, so I have to extract the items manually and this code:
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); |
You had mentioned that you use a Collection and the NeedDataSource handler, just as I do. I found as you did that the place we lose the edits is on Rebind, usually following a full postback or a call to that method. Therefore, if I modify the bound data during the NeedDataSource handler (before it's bound), it will always be right.
I don't know if the PreRender runs often without the NeedDataSource firing first, but since I do multi-row edits, I have to call Rebind() before it goes out. (I still don't understand that one.) Since I call Rebind(), it naturally goes through NeedDataSource again.
I use mostly Template columns on the form in question, and the use of
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); |
Graeme
This is interesting. All my GridTemplateColumn bound fields come through fine. However, I have one GridTemplateColumn which I bind from ItemDataBound event handler. As a consequence, the value is not directly available for ExtractValuesFromItems.
Now I have modified CacheEditValues to accommodate that one column by requesting its value specifically, and manually adding it to the Hashtable:
private void CacheEditValues() | |
{ | |
foreach (GridEditableItem editItem in this.PartsToOrderViewRadGrid.EditItems) | |
{ | |
Hashtable newValues = new Hashtable(); | |
object key = editItem.OwnerTableView.DataKeyValues[editItem.ItemIndex]["PartID"]; | |
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem); | |
//doesn't pick up the DefaultVendorID from the combo | |
long defaultVendorID = Convert.ToInt64((editItem.FindControl("PartVendorCombo") as RadComboBox).SelectedValue); | |
newValues.Add("DefaultVendorID", defaultVendorID); | |
hashList.Add(key, newValues); | |
} | |
} | |
"Works a treat!" as they say in England (is that where They say it? ;-)
Graeme
Most of my binding is done with this syntax:
Text='<%# (DirectCast(Container.DataItem, Png.Gcs35.Domain.Deal).BACell) %>' |
As in this example:
<telerik:GridTemplateColumn HeaderText="Bus. Associate" UniqueName="BusAssociateColumn" |
SortExpression="BACell"> |
<HeaderStyle Width="200px" HorizontalAlign="Left" /> |
<ItemTemplate> |
<asp:Label ID="LabelBACell" runat="server" Text='<%# (DirectCast(Container.DataItem, Png.Gcs35.Domain.Deal).BACell) %>' /> |
</ItemTemplate> |
<EditItemTemplate> |
<table border="0" cellpadding="0" cellspacing="0" style="border-left: none; border-bottom: none;" |
width="100%"> |
<tr> |
<td style="border-left: none; border-bottom: none;" width="100%"> |
<asp:Label ID="LabelBACellEdit" runat="server" Text='<%# (DirectCast(Container.DataItem, Png.Gcs35.Domain.Deal).BACell) %>' /> |
</td> |
</tr> |
<tr> |
<td style="border-left: none; border-bottom: none;" width="100%"> |
<pnggcs:BusinessAssociateComboBox ID="BusinessAssociateComboBox1" runat="server" |
AutoPostBack="true" Style="margin-right: 0px" Width="195px" DropDownWidth="400px" |
OnSelectedIndexChanged="BusinessAssociateComboBox1_SelectedIndexChanged" /> |
</td> |
</tr> |
</table> |
</EditItemTemplate> |
</telerik:GridTemplateColumn> |
As the GridDropDownColumn doesn't do everything :-), we end up using Templates. In my situation, I needed to support the loading of items in my combo via web service. This is what I ended up doing (using a RadComboBox, in this case):
<radx:GridTemplateColumn UniqueName="DefaultVendorIDCol" HeaderText="Vendor" DataField="DefaultVendorID" | |
SortExpression="DefaultVendorID"> | |
<ItemTemplate> | |
<asp:Label ID="VendorLabel" runat="server"></asp:Label></ItemTemplate> | |
<EditItemTemplate> | |
<radx:RadComboBox ID="PartVendorCombo" runat="server" DataTextField="PartVendorName" | |
DataValueField="PartVendorID" Skin="WebBlue" EnableLoadOnDemand="true" Height="240px" | |
AllowCustomText="false" ShowMoreResultsBox="false" NoWrap="false" MaxLength="500" | |
OnClientItemsRequesting="comboItemsRequesting" OnClientItemsRequested="comboItemsRequested" | |
OnClientSelectedIndexChanged="comboIndexChanged"> | |
</radx:RadComboBox> | |
</EditItemTemplate> | |
</radx:GridTemplateColumn> | |
Graeme