The EntitySpaces Community

Share and learn about the EntitySpaces Architecture.
Welcome to The EntitySpaces Community Sign in | Join | Help
in
Home Forums Photos

DataGridView with modal dialog to add/edit records

Last post 09-08-2008, 7:54 AM by quimbo. 9 replies.
Sort Posts: Previous Next
  •  04-25-2008, 11:40 AM 9030

    DataGridView with modal dialog to add/edit records

    I have the same question posted in this thread: Examples of forms with grid and modal dialogs to add/edit rows?

    (I'm using the latest trial version of ES2007, downloaded just a few days ago. MyGeneration 1.3. postgresql 8.3. Visual C# 2008 Express Edition.)

    I have a read-only DataGridView. There is an "edit" button in each row. The edit button opens a tiny dialog box. The user is meant to do their editing within this dialog rather than directly on the DataGridView. The other requirement is that I'd like to use data binding. I've had a degree of success, but I just can't nail it.

    So there is a Phone object, and a PhoneCollection object. On the main form, I have a DataGridView bound to the PhoneCollection. The user clicks the "edit" button, and a form pops up called EditPhone.

    The following code is tangential, but in case others would like to do the same thing, here's the code to accomplish that much:

    Code:
            private void grdPhone_CellContentClick(object sender, DataGridViewCellEventArgs e)
            {
                if (IsNonHeaderButtonCell(this.grdPhone, e)) {
                    EditPhone edit = new EditPhone(phoneCollection[e.RowIndex]);
                    edit.ShowDialog(this);
                }
            }
            private bool IsNonHeaderButtonCell(DataGridView grd, DataGridViewCellEventArgs cellEvent)
            {
                if (grd.Columns[cellEvent.ColumnIndex] is DataGridViewButtonColumn
                    && cellEvent.RowIndex != -1) { return true; } else { return (false); }
            }
    

    (This works fine, although I'm not sure the index into the grid - e.RowIndex - will always correspond with the index in the phoneCollection. I'll figure this out later.)

    EditPhone is a little dialog box that allows the user to edit the phone record. Thanks to design-time data binding, there's not much code to EditPhone. Here it is:

    Code:
        public partial class EditPhone : Form
        {
            Phone phone;
            public EditPhone(Phone phoneToEdit)
            {
                InitializeComponent();
                phone = phoneToEdit;
                this.bindingSource.DataSource = phone;
            }
            private void btnCancel_Click(object sender, EventArgs e)
            {
                phone.RejectChanges();
            }
            private void btnOk_Click(object sender, EventArgs e)
            {
            phone.AcceptChanges();
            }
        }

        
    This kind-of works, but with some problems.

    1. Back on the main form with the DataGridView, when I save the phoneCollection, no changes are saved. That's because of my call to AcceptChanges().
    2. As the user makes changes to the dialog, the changes are immediately reflected on the DataGridView in the background. While this is actually kind of cool looking, it's not the behavior most users expect. I'd prefer for the changes to be reflected only after clicking "Ok."


    If I remove RejectChanges(), then clicking Cancel on EditPhone has no effect -- the user's changes are applied anyways.

    If I remove AcceptChanges(), then it solves problem #1 listed above, but introduces a new problem: if the user edits a phone multiple times, the Cancel button will revert the record to its original values rather than the values as of the last time the Ok button was clicked.

    I've tried using CancelEdit() and EndEdit() on bindingSource, but those methods seem to have no effect whatsoever.

    Of course one way to accomplish what I want is just to NOT use data binding. This is a last resort, though, because data binding is so convenient and makes the code easier to manage.
     

    Thanks!

    Patrick

     

  •  04-29-2008, 2:28 PM 9102 in reply to 9030

    Re: DataGridView with modal dialog to add/edit records

    Okkaayy, after much trial and error, I have something that seems to work.

    I'm creating a contact database. My main entity is a Person. The related entities are Phone, Address, Email, etc. There is a form that allows users to view and edit Person data. The related tables are represented by read-only DataGridViews. For example, there is a DataGridView bound to Person.PhoneCollection.

    While users cannot edit records directly in a DataGridView, they can click Add or Edit buttons. Clicking one of these buttons opens a little dialog box, where the user can add a record or modify an existing record. This dialog has the standard "Okay" and "Cancel" buttons. If the user clicks "Cancel", the current changes are discarded. If the user clicks "Okay", then the changes they've made to the current Phone record are reflected back in the DataGridView. However, the changes are not saved until the user clicks Save on the main Person form.

    I've tried a million different combinations, and here's what works so far.

    When the main form opens (which is bound to a Person record), the DataGridView that lists the person's phone numbers is bound to the related PhoneCollection: 

     

    Code:
                phoneCollection = person.PhoneCollectionByPersonId;
    this.grdPhone.DataSource = phoneCollection;

    If the user clicks the Add button located above the DataGridView, the following code is executed:

     

    Code:
            private void btnAddPhone_Click(object sender, EventArgs e)
    {
    Phone newPhone = new Phone();
    EditPhone edit = new EditPhone(newPhone);
    DialogResult result = edit.ShowDialog(this);
    if (result == DialogResult.OK) {
    newPhone.PersonId = person.Id;
    phoneCollection.AttachEntity(newPhone);
    }
    }
     

    So, a new Phone object is created and then sent to the dialog box (a form called EditPhone). When the user clicks "Okay" on the dialog, the foreign key is set properly, and the record is added to the PhoneCollection using the AttachEntity method. This works perfectly. Thanks to data binding, the new record automatically pops into the DataGridView. The new Phone record has not been saved, though.

    Rather than having a single Edit button, I put an Edit button in each row of the DataGridView. When that button is clicked, the corresponding row in the DataGridView should be opened in the dialog box.

    I tried extracting a Phone record from the PhoneCollection and simply sending that to EditPhone, my dialog box. It worked perfectly for newly added records that hadn't yet been saved. But for old records that had been saved, it didn't work as I hoped. Here is where my code gets a little kludgy. I found I could create a duplicate Phone record, send THAT to EditPhone, and then copy the results back to the Phone record in the PhoneCollection.

     

    Code:
            private void grdPhone_CellContentClick(object sender, DataGridViewCellEventArgs e)
    {
    if (IsNonHeaderButtonCell(this.grdPhone, e)) {
    Phone phoneToEdit = phoneCollection[e.RowIndex];
    Phone copyToEdit = new Phone();
    EditPhone edit;
    if (phoneToEdit.es.IsAdded) {
    edit = new EditPhone(phoneToEdit);
    } else {
    CopyEntity(phoneToEdit, copyToEdit);
    edit = new EditPhone(copyToEdit);
    }
    DialogResult result = edit.ShowDialog(this);
    if ((phoneToEdit.es.IsAdded == false) && (result == DialogResult.OK)) {
    CopyEntity(copyToEdit, phoneToEdit);
    phoneToEdit.PersonId = person.Id;
    }
    }
    }
    private bool IsNonHeaderButtonCell(DataGridView grd, DataGridViewCellEventArgs cellEvent)
    {
    if (grd.Columns[cellEvent.ColumnIndex] is DataGridViewButtonColumn
    && cellEvent.RowIndex != -1) { return true; } else { return (false); }
    }
    private void CopyEntity(EntitySpaces.Core.esEntity original, EntitySpaces.Core.esEntity copy)
    {
    foreach (EntitySpaces.Interfaces.esColumnMetadata c in original.es.Meta.Columns) {
    copy.SetProperty(c.PropertyName, original.GetColumn(c.PropertyName));
    }
    }

    The code for EditPhone is quite simple. It's a form with a few fields, which are bound to a BindingSource. When I created the form I set the DataSource to a Phone record, but then removed it and set it programmatically. Here is the EditPhone class:

     

    Code:
        public partial class EditPhone : Form
    {
    public EditPhone(Phone phoneToEdit)
    {
    InitializeComponent();
    this.bindingSource.DataSource = phoneToEdit;
    }
    private void btnCancel_Click(object sender, EventArgs e)
    {
    this.bindingSource.CancelEdit();
    }
    private void btnOk_Click(object sender, EventArgs e)
    {
    this.bindingSource.EndEdit();
    }
    }

    I actually think the code for btnOk_Click isn't even necessary. The code for btnCancel_Click is only necessary for records that were recently added but haven't yet saved.

    When the user clicks the Save button back on the main Person form, the code couldn't be simpler:

     

    Code:
                    person.Save();
     

    That automatically saves the related PhoneCollection. If the user never clicks the Save button, then the changes to the PhoneCollection are never committed to the database, which is the desired behavior.

    So there you have it. For people who want to use EntitySpaces, read-only DataGridViews, and modal dialog boxes to add/edit records, this solution seems to work.

    However, my solution feels kludgy. I'm not even sure why some parts of it work. If somebody has a more elegant solution, please post it on this thread.

    Thanks!

    Patrick
     

  •  04-29-2008, 2:53 PM 9103 in reply to 9102

    More thoughts about the kludgy parts

    I suspect I'm just talking to myself here. But I do hope somebody else finds all this useful at some point.

    There is one part in all of this I still don't understand:

    If you have a DataGridView bound to an entity collection (like my PhoneCollection)...

    ...and then you grab a single item from the collection (in my case, a Phone object)...

    ...and then you bind a Form to this object (in my case, an EditPhone form)...

    ...and then the user makes changes to the data on that form...

    ...those changes SHOULD be reflected back on the DataGridView as the user works.

    And in fact, this is exactly what happens most of the time. While this behavior is expected, it is not desired. (It could confuse users to see the DataGridView change in the background as they make changes in a modal dialog box.) I got around this by creating a duplicate of the Phone object and having the user work on the duplicate.

    However, if a record is added to the collection using AttachEntity, it does not follow this behavior. Instead, changes that the user makes on the Form are not relfected back in the DataGridView until there is a call to EndEdit() on the BindingSource.

    Why is this so? What's going on under the hood?

    What is the real difference between this... 

     

    Code:
    Phone phone = phoneCollection.AddNew();

     

    ...and this... 

     

    Code:
    Phone phone = new Phone();
    phoneCollection.AttachEntity(phone);
     

    ??

  •  04-29-2008, 8:33 PM 9105 in reply to 9103

    Re: More thoughts about the kludgy parts

    First, I'd like to assure you that you are not posting in a vacuum. I have been following along, with interest, because I, too, prefer to code read-only grids with modal add/edit dialogs. But, my design approach is a little different, so I did not feel I could contribute much to your specific issue.

    I'll try to address what's going on under the hood with EntitySpaces, but must confess that what's going on under the hood with a DataGridView stills seems like black magic to me. An entity can be part of a collection, or a stand-alone entity. When you call collection.AddNew(), not only is entity.AddNew() called, but the entity's Collection property is set. When you call entity.AddNew() the Collection property remains null until you call the AttachEntity() method. Since your DGV is bound to the collection, whether, or not, the entity is part of that collection is going to contribute to the DGV's behavior.

    As far as design approach, it is determined mainly based on end-user behavior that I have observed over the years, and in supporting 3rd party apps, some of which I felt handled that behavior better than others. The apps I've written are typically small multi-user apps, run over an internal network, with a hand-full of administrative users with edit rights, and the bulk of the users with read-only rights. The currency of the data is of primary importance, and the effect on network traffic is negligible.

    One observation is that users often load a grid, are then interrupted, leave the grid open while they work on a "super-high-priority" job, then return much later to the "high-priority" job that they originally started.

    The second is that it seems that no matter how many times you warn them, until they get burned at least once themselves, users are not going to hit save often enough, if you let them accumulate adds/edits and save the batch at their discretion. This has a number of effects. Other users may have edited and saved one of the records, causing a concurrency issue. Other users are not seeing the most up-to-date information. And, users will spend an hour entering data, head out to lunch without hitting save, only to return and find that the power went out while they were gone.

    I address these by passing primary key(s) to the form in edit mode, rather than passing the entity from the previously loaded collection. The form does a LoadByPrimaryKey in edit mode, and AddNew in add mode. The form's OK saves the changes, before closing. LoadByPrimaryKey ensures that the user is editing current data, and the save takes responsibility of saving a batch out of their hands.

    We need to put together a sample app that demonstrates this, but until we get ES2008 out, it is kind of on the back-burner.


    David Neal Parsons
    www.entityspaces.net
  •  05-09-2008, 3:53 PM 9244 in reply to 9103

    Re: More thoughts about the kludgy parts

    Hi Patrick

    A little late in terms of a reply but as David has already said, you're not talking to yourself don't worry - I've "pinned" the thread for future reference etc too :D

    Anyway, just for kicks I've spent a few minutes this evening seeing what I could come up with and managed to achieve something along the lines of what you're discussing (i.e. changes made in the dialog/edit form aren't reflected in realtime on the grid, only when accepted, and newly added items are shown when they are accepted/entered by the user on the dialog).

    Code snippets below but a quick couple of notes first. 

    Firstly, it assumes the edit dialog is a modal form as it actually calls save on the collection rather than the entity (therefore you won't be able to have a number of changes to different entities in the grid all pending changes/db updates) - this suits some applications well, but obviously not all applications want to work this way.

    Secondly, while your edit/create dialog is open, the grid won't show any other changes - namely, if the collection you're showing in the grid can be updated outside of the form itself (e.g. it's a 'shared' collection) and you want these changes reflected in real time then again, this isn't the solution for you as all (visual) updates to the grid are temporarily put on hold while the edit dialog is open.

    Anyway, onto the (relevant) code snippets:

    This is for the grid form (which has a DataGridView on it along with a Bindingsource.  The Datasource of the DGV is set to the Bindingsource

    1) load my collection

    Code:
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Dim outcomeList as New CallOutcomeCollection
     
        EntitySpaces.Interfaces.esProviderFactory.Factory = New EntitySpaces.LoaderMT.esDataProviderFactory()
         outcomeList.LoadAll()
        Me.BindingSource1.DataSource = outcomeList
    End Sub
    

    Datagridview doubleclick event handler is as follows - basically checks whether we're in the "NewRow" or double clicking on an existing item in the grid.  Depending on which we will either change the "RaiseListChangedEvents" property to False, or leave it alone.  We then create a new "Edit form" and pass the entity to it - this passed entity will either be a newly created entity, or the existing one that the user wants to edit. NB - code doesn't check if click is on header cell or have any error handling as it's a quick n dirty attempt from me :D

    Code:
    Private Sub DataGridView1_CellDoubleClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellDoubleClick
        Dim entity As CallOutcome
        Dim IsNewRow As Boolean = e.RowIndex.Equals(DataGridView1.NewRowIndex)
    
        'If we're adding a new row then leave RaiseListChangedEvents = True, if not 
        'then change it to False so changes made in the edit dialog aren't reflected
        'immediately in the grid
        BindingSource1.RaiseListChangedEvents = IsNewRow
    
        If e.RowIndex = DataGridView1.NewRowIndex Then
            entity = BindingSource1.AddNew
        Else
            entity = CType(BindingSource1.Current, CallOutcome)
        End If
    
        Dim add As New Entity_Edit_form(entity)
        add.ShowDialog()
        BindingSource1.RaiseListChangedEvents = True
    End Sub

    That's it in the form holding the grid.

    The code on the Edit/Create new entity form is simple enough. 

    The form has textboxes to enter the required entity data, these are bound to a Bindingsource.

    1) Ctor: This accepts an entity (passed from the DGV form) in the constructor

    Code:
    Public Sub New(ByVal Outcome As CallOutcome)
        InitializeComponent()
        Me.CallOutcomeBindingSource.DataSource = Outcome
    End Sub

     

    2) "OK Button" - this saves your changes (or new entity) by calling save on the entities "Collection" property

    Code:
    Private Sub BtnOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnOK.Click
        CallOutcomeBindingSource.Current.Collection.Save()
        Close()
    End Sub

     

    3) "Cancel Button" - calls "Rejectchanges" on the collection and also calls CancelEdit on the bindingsource (if you only call "CancelEdit" then the changes are still accepted by the collection and show up in the DGV, if you only call "RejectChanges" then this works fine except for if you add a new row, then press the cancel button - effectively you're left with a blank row in your grid that will throw exceptions if you're not careful.  Calling both of these methods seems to do the trick nicely).

    Code:
    Private Sub BtnCancel_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles BtnCancel.Click
        CallOutcomeBindingSource.Current.collection.RejectChanges()
        CallOutcomeBindingSource.CancelEdit()
        Close()
    End Sub
    

     

    Anyway, thought I'd post it up here incase it helps provide any help (plus it'll act as a reminder for me if I need to look at doing this myself in the future!)

    Cheers

    Martin

     

  •  09-04-2008, 2:08 PM 11175 in reply to 9244

    Re: More thoughts about the kludgy parts

    i decided to look into this sample and have run into a problem.  this line:

    CallOutcomeBindingSource.Current.Collection.Save()

     throws an error:
    Option Strict On disallows late binding.
     Trying to figure out how to code for that.  Current does not have a Collection method/property 
     
    Code:
      Private supplyBindSource As BindingSource


    Public Sub New(ByVal Supply As BusinessObjects.Supply)
    InitializeComponent()
    supplyBindSource = New BindingSource
    supplyBindSource.DataSource = Supply

    End Sub

    Private Sub
    SigEdit_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    BindFields()

    End Sub

    Private Sub
    uxOK_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles uxOK.Click

    supplyBindSource.Current.Collection.Save()
    supplyBindSource.EndEdit()

    Close()

    End Sub


    Private Sub
    uxCancel_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles uxCancel.Click

    supplyBindSource.Current.Collection.RejectChanges()
    supplyBindSource.CancelEdit()
    Close()
    End Sub
     

     

  •  09-05-2008, 8:57 PM 11196 in reply to 11175

    Re: More thoughts about the kludgy parts

    Hi!

     Just add CType()

    Code:
     Private Sub uxOK_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles uxOK.Click        
    CType(supplyBindSource.Current, BusinessObjects.Supply).Collection.Save()        
    supplyBindSource.EndEdit()        
    Close()    
    End Sub    
    
    Private Sub uxCancel_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles uxCancel.Click        
    CType(supplyBindSource.Current, BusinessObjects.Supply).Collection.RejectChanges()        
    supplyBindSource.CancelEdit()        
    Close()    
    End Sub
  •  09-06-2008, 5:24 AM 11202 in reply to 11196

    Re: More thoughts about the kludgy parts

    thanx much
  •  09-06-2008, 8:57 AM 11205 in reply to 11202

    Re: More thoughts about the kludgy parts

    I think you need to call EndEdit() on your BindingSource, before you call Save() on the collection.
    David Neal Parsons
    www.entityspaces.net
  •  09-08-2008, 7:54 AM 11227 in reply to 11205

    Re: More thoughts about the kludgy parts

    The CType was what I needed to correct the late binding errors,

    i will also put int eh call to endedit

     

    This is a nice technique for modal editing off af a gridview

     

View as RSS news feed in XML