This is a migrated thread and some comments may be shown as answers.

Multi-add and Multi-edit for Grid

16 Answers 516 Views
Grid
This is a migrated thread and some comments may be shown as answers.
Simon Kingaby
Top achievements
Rank 2
Simon Kingaby asked on 08 Aug 2008, 05:54 PM
I need to be able to allow users to add or edit multiple rows in a RadGrid.  For example, they may open the grid page and see three rows.  They click the Edit button in row 2 and row 3.  They add a new row 4, then another new row 5.

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

Sort by
0
Yavor
Telerik team
answered on 12 Aug 2008, 07:16 AM
Hello Simon,

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.
0
The Oracle
Top achievements
Rank 1
answered on 16 Sep 2008, 06:36 PM
How did this turn out?  I have a grid with multiple rows in edit mode, and I want to issue an Update from an outside button.  If there is an error, the error is displayed in my error div, but the changes to the edited records are all lost.

Please help!

Thanks,
Graeme
0
Yavor
Telerik team
answered on 17 Sep 2008, 07:35 AM
Hello,

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.
0
Simon Kingaby
Top achievements
Rank 2
answered on 17 Sep 2008, 02:06 PM
So here is the solution I came up with.  

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 ObjectByVal 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 ObjectByVal 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  

0
Simon Kingaby
Top achievements
Rank 2
answered on 17 Sep 2008, 02:11 PM
The ScrapeAllRows code above is the code that I used to handle the multi-edit feature where you have one or more rows in edit mode, then click edit on another row, or execute any command in the grid, only to watch the grid refresh and wipe out all the changes made so far.

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.
0
Simon Kingaby
Top achievements
Rank 2
answered on 17 Sep 2008, 02:48 PM
As far as the Multi-Add.   Here is the code that makes that work.  In combination with the ScrapeAllRows code, this makes for a RadGrid that you can multi-add, and multi-edit, without losing changes made so far.

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 ObjectByVal 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 ObjectByVal 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 ObjectByVal 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).



0
The Oracle
Top achievements
Rank 1
answered on 17 Sep 2008, 06:32 PM

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

0
The Oracle
Top achievements
Rank 1
answered on 17 Sep 2008, 06:55 PM
Well, that bites.  It was working, but now is not.  I guess you get the idea, but you really need some method of row control.  This is too rigid.

Graeme
0
The Oracle
Top achievements
Rank 1
answered on 17 Sep 2008, 07:24 PM
OK, it works, if you use ItemIndex instead of RowIndex in Item_DataBound.  But it's best to use row control, like you did, or thus:

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
0
The Oracle
Top achievements
Rank 1
answered on 17 Sep 2008, 09:46 PM
SIMON!!

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;  
 
0
The Oracle
Top achievements
Rank 1
answered on 17 Sep 2008, 10:05 PM
Hey, Simon--

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
0
Simon Kingaby
Top achievements
Rank 2
answered on 18 Sep 2008, 12:56 PM
When you use the OnDataBind instead of the PreRender event, are you seeing the code run more or less often?  Is the performance better?  Thx.

Most of my columns use ItemTemplates and EditTemplates, so I have to extract the items manually and this code: 
editItem.OwnerTableView.ExtractValuesFromItem(newValues, editItem);   
won't work for me.  Otherwise, that's a cool twist on the collection based solution.
0
The Oracle
Top achievements
Rank 1
answered on 18 Sep 2008, 02:46 PM
Hey, Simon--

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); 
...works for me.  I'm using the <%# Bind("..") %> constructs.  Are you doing something different?

Graeme
0
The Oracle
Top achievements
Rank 1
answered on 18 Sep 2008, 03:37 PM
Dear Simon,

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
0
Simon Kingaby
Top achievements
Rank 2
answered on 18 Sep 2008, 04:38 PM
80% of the columns in my grid are Combos, so I have the same issue as your exception, and it never occurred to me to pull the ExtractValues. 

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> 
 
0
The Oracle
Top achievements
Rank 1
answered on 18 Sep 2008, 05:12 PM
So is it possible to reach your custom BusinessAssociateComboBox and read the value like I did?  And your SelectedIndexChanged causes the PostBack, doesn't it?

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
Tags
Grid
Asked by
Simon Kingaby
Top achievements
Rank 2
Answers by
Yavor
Telerik team
The Oracle
Top achievements
Rank 1
Simon Kingaby
Top achievements
Rank 2
Share this question
or