none
Canceling changes made to a class passed to a dialog. RRS feed

  • Question

  • Hi Everyone,

    Here we do again. I have a class that has a few variables and lists of integers.

    I have a form I show as dialog and I pass the class to the form in the new contructor.

    This is the dialog form code to pass the class to the form:

        Private thisGame1 As Game
        Private thisGameBU As Game


        Public Sub New(ByVal thisGame As Game)

            ' This call is required by the designer.
            InitializeComponent()

            ' Add any initialization after the InitializeComponent() call.
            thisGameBU = thisGame
            thisGame1 = thisGame

        End Sub


    Then I put the values in the class into a control on the form that uses the class data and displays it, and lets the user change the data.

    Then when the form closes if the user wants to save the changes I copy the data from the control to the original class and it returns to the main form with the changed class data.

    So that all works fine.

    Now when closing the dialog if the user elects to cancel the changes I dont want the original class data to be changed. So how do I reset it?

    As you can see I tried making a copy and using that but is appears byref so I guess the copy is changing along with the the rest?

       thisGame1 = thisGameBU

    First off the byval in the New sub seems to have no effect it seems to be byref?

    So I seems last time I hit this I ended up manually copying the whole class one variable at a time so the copy was not linked by ref to the original.

    Is there a better way to do this canceling?

    I mean its is fine unless one wishes to cancel the changes and then I cant figure a good way to restore the class to its original data when the dialog was first called ie cancel the changes made in the dialog. It always seems linked byref?

    Tuesday, May 9, 2017 2:45 PM

Answers

  • Hi

    Here is my understanding of your question.

    To use a copy of the Class, I use a Clone method )see attribute in code block). This allows the copy to be used without changing the original, or, to set original to new values.Just set breakpoints to see effects.

    This may be way off track,but here it is anyway.

    Option Strict On
    Option Explicit On
    Imports System.IO
    Imports System.Runtime.Serialization.Formatters.Binary
    
    Public Class Form1
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Dim thisGame1 As game = New game
            thisGame1.One = 11
            thisGame1.Two = 22
            thisGame1.Three = "Freddy"
    
            ' make clone copy of thisGame1
            ' thisGame1 remains untouched
            Dim thisGameBU As game = DirectCast(thisGame1.Clone, game)
            thisGameBU.One = 111
            thisGameBU.Two = 222
            thisGameBU.Three = "Mary"
    
            ' reset thisGameBU to thisGame1
            thisGameBU = DirectCast(thisGame1.Clone, game)
    
            ' OR, if original to be updated to new values
            thisGame1 = DirectCast(thisGameBU.Clone, game)
    
        End Sub
    End Class
    
    ' made from http://www.rectanglered.com/deep-copying-object-vb-net
    <Serializable()>
    Public Class game
        Implements ICloneable
    
        Public One As Integer = 0
        Public Two As Integer = 0
        Public Three As String = String.Empty
    
        Public Function Clone() As Object Implements ICloneable.Clone
            Dim m As New MemoryStream()
            Dim f As New BinaryFormatter()
            f.Serialize(m, Me)
            m.Seek(0, SeekOrigin.Begin)
            Return f.Deserialize(m)
        End Function
    End Class


    Regards Les, Livingston, Scotland

    • Marked as answer by tommytwotrain Tuesday, May 9, 2017 11:55 PM
    Tuesday, May 9, 2017 4:09 PM
  • Tommy,

    Because what I wrote to Les, I realized me, that I made a mistake in my reply, because if you copy a references it still the same object which is references. 

    However, when I tried to make the sample my solution was different and it is easy, beside that code is what I wrote true.. 

    dim x = oldgame
    oldgame = new Game.

    Now x, is the reference to the old object and oldgame is a complete different object. 

    the object inside x is not released because the GC looks if something has a reference and that is now x.


    Success
    Cor

    Tuesday, May 9, 2017 5:08 PM

All replies

  • Hi Tommy,

    Maybe you know this already but let me do if you don't otherwise it is to difficult to explain. I tried to make a sample but, I don't direct see where the Game comes from in your text above, because it is in the constructor so it should be provided from outside the program. 

    Because of courses in the past which were always about static Classes where the Class is direct is an object it is mostly difficult to make that little step why OOP is so easy to use. 

    A class in OOP is nothing more than an Template, you have to think in objects. 

    You construct an object in VB from a class with the Keyword New. As long as an object is not constructed it stays at the same address and only the contents change. 

    <strike></strike>

    I assume it is senseless to show you that in this way you can make a copy of your game object and restore it afterwards for you.


    Success
    Cor





    Tuesday, May 9, 2017 3:58 PM
  • Hi

    Here is my understanding of your question.

    To use a copy of the Class, I use a Clone method )see attribute in code block). This allows the copy to be used without changing the original, or, to set original to new values.Just set breakpoints to see effects.

    This may be way off track,but here it is anyway.

    Option Strict On
    Option Explicit On
    Imports System.IO
    Imports System.Runtime.Serialization.Formatters.Binary
    
    Public Class Form1
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Dim thisGame1 As game = New game
            thisGame1.One = 11
            thisGame1.Two = 22
            thisGame1.Three = "Freddy"
    
            ' make clone copy of thisGame1
            ' thisGame1 remains untouched
            Dim thisGameBU As game = DirectCast(thisGame1.Clone, game)
            thisGameBU.One = 111
            thisGameBU.Two = 222
            thisGameBU.Three = "Mary"
    
            ' reset thisGameBU to thisGame1
            thisGameBU = DirectCast(thisGame1.Clone, game)
    
            ' OR, if original to be updated to new values
            thisGame1 = DirectCast(thisGameBU.Clone, game)
    
        End Sub
    End Class
    
    ' made from http://www.rectanglered.com/deep-copying-object-vb-net
    <Serializable()>
    Public Class game
        Implements ICloneable
    
        Public One As Integer = 0
        Public Two As Integer = 0
        Public Three As String = String.Empty
    
        Public Function Clone() As Object Implements ICloneable.Clone
            Dim m As New MemoryStream()
            Dim f As New BinaryFormatter()
            f.Serialize(m, Me)
            m.Seek(0, SeekOrigin.Begin)
            Return f.Deserialize(m)
        End Function
    End Class


    Regards Les, Livingston, Scotland

    • Marked as answer by tommytwotrain Tuesday, May 9, 2017 11:55 PM
    Tuesday, May 9, 2017 4:09 PM
  • Les,

    OT, a clone is a word used in .Net for all kind of things. The word for your code is making a Deep Copy

    (In most classes is a clone a copy of an object where a reference to the same object is created but where the data is not included. )

    https://en.wikipedia.org/wiki/Object_copying#Deep_copy

    (The name method "copy" and "clone" are in .Net very much used for different implementations)

    Just a hint not as meant as answer.

    In my sample I tried to avoid that serialization way. 


    Success
    Cor

    Tuesday, May 9, 2017 4:30 PM

  • Hi Tommy,

    Maybe you know this already but let me do if you don't otherwise it is to difficult to explain. I tried to make a sample but, I don't direct see where the Game comes from in your text above, because it is in the constructor so it should be provided from outside the program. 

    Because of courses in the past which were always about static Classes where the Class is direct is an object it is mostly difficult to make that little step why OOP is so easy to use. 

    A class in OOP is nothing more than an Template, you have to think in objects. 

    You construct an object in VB from a class with the Keyword New. As long as an object is not constructed it stays at the same address and only the contents change. 

    Therefore if you say:

    Dim x as game = OldGame. You have twice a reference to the same object. 

    However if you say:

    dim x as game = new Game with {.x = OldGame.x, .y=Oldgame.y, .......} then you have made a copy of the object. 

    I assume it is senseless to show you that in this way you can make a copy of your game object and restore it afterwards for you.


    Success
    Cor


    Well its complicated so I was hoping it was such a common thing that you would spot my problem.

    Maybe you did. This is what I was using. The game1 is declared as a new game in the main form. Some data added to it in form load. Then I call the dialog from a button click as shown.

        Public Game1 As New Game
    
       Private Sub TileEditorBtn_Click(sender As Object, e As EventArgs) Handles TileEditorBtn.Click
            Using te As New TileEditor(Game1)
                te.ShowDialog()
    
            End Using
        End Sub

    So I just tried this but same problem. Once I set anything equal to the current data in game1 then if I change that data game1 changes.

    Private Sub TileEditorBtn_Click(sender As Object, e As EventArgs) Handles TileEditorBtn.Click

    Dim anotherGame As New Game anotherGame = Game1 Using te As New TileEditor(anotherGame) te.ShowDialog() End Using End Sub


    The Game class is big but its just for data in this case. The game class has no contructor code. Its just simple stuff like you see I know how to do. Basically here is parts of the data part :

    <Serializable()> Public Class Game
        Private _sprites As New List(Of Sprite)
        Private _mapTiles As New List(Of MapTile)
        Private _gridSize As Size
        Private _mapOriginPt As Point
        Private _scaleFactor As Single
        Private _tilesBmp As Bitmap
        Private _tileMapProperties As New List(Of MapTile)
        Private _fileSaved As Boolean
    
        <Serializable()> Public Class Animation
            Public Style As Integer
            Public Location As PointF
            Public Frame As Integer = -1
            Public Message As String = ""
            Public Angle As Single
        End Class
        Public Animations As New List(Of Animation)
    
    
        Public Property Sprites As List(Of Sprite)
            Get
                Return _sprites
            End Get
            Set(value As List(Of Sprite))
                _sprites = value
            End Set
        End Property


    I will try to make an example if this does not help.

    Leshay, give me some time to look at your copy code. Seems I tried just cloning the class I dont recall this go around. Again I am just trying to learn the "proper" way if there is one.

    Yes I was making a copy of the class so I could cancel changes by reverting to the copy. But I cant get a linked ref free copy. Editor seems buggy today? Now I cant add a line.

    Tuesday, May 9, 2017 4:41 PM
  • PS So I just want to pass the data in the class to the dialog form and modify it. If the user changes mind and does not want to modify the original data then I want to exit without modifying.

    But I need to get game1 data to the dialog at the start. Then change that data, then either return the modified data or if desired cancel and return unmodified data or whatever works.

    Tuesday, May 9, 2017 4:58 PM
  • Tommy,

    Because what I wrote to Les, I realized me, that I made a mistake in my reply, because if you copy a references it still the same object which is references. 

    However, when I tried to make the sample my solution was different and it is easy, beside that code is what I wrote true.. 

    dim x = oldgame
    oldgame = new Game.

    Now x, is the reference to the old object and oldgame is a complete different object. 

    the object inside x is not released because the GC looks if something has a reference and that is now x.


    Success
    Cor

    Tuesday, May 9, 2017 5:08 PM
  • Try to think in Objects as references to an object. 

    Dim x as TheGame
    Dim TheGame = new Game

    dim TheGame = x

    What happens now, TheGame contains the data from the old game. 
    x is referencing the same, but that is no problem, the next time it gets the new reference. 

    However, the Game which object you have new Constructed in this code, will be released. That object has lost its reference because the reference of the Game is again to the old Game. 

    Be aware to dimension this with Private on Global level otherwise they go out of scope.


    Success
    Cor


    Tuesday, May 9, 2017 5:22 PM
  • Tommy, 

    I'm always fighting with that ShowDialog, however here a sample what I mean. 

    Public Class Form1
        Private ThisTommy As New Tommy("")
        Private OldTommy As Tommy
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            ThisTommy.Text = TextBox1.Text
            OldTommy = ThisTommy
            Using ofd As New Form2(ThisTommy)
                ofd.TextBox1.Text = ThisTommy.Text 'a little bit dirty for this sample
                If ofd.ShowDialog <> DialogResult.OK Then
                    ThisTommy = OldTommy
                End If
                TextBox1.Text = ThisTommy.Text
            End Using
        End Sub
    End Class
    Public Class Tommy
        Public Property Text As String
        Public Sub New(Text As String)
            Me.Text = Text
        End Sub
    End Class
    Public Class Form2
        Friend Property ThisTommy As Tommy
        Public Sub New(ThisTommy As Tommy)
            InitializeComponent()
            Me.ThisTommy = ThisTommy
            TextBox1.Text = ThisTommy.Text
        End Sub
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            DialogResult = DialogResult.OK
            Me.Close()
        End Sub
        Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles TextBox1.TextChanged
            ThisTommy.Text = TextBox1.Text
        End Sub
    End Class


    Success
    Cor

    Tuesday, May 9, 2017 6:17 PM
  • Try to think in Objects as references to an object. 

    Dim x as TheGame
    Dim TheGame = new Game

    dim TheGame = x

    What happens now, TheGame contains the data from the old game. 
    x is referencing the same, but that is no problem, the next time it gets the new reference. 

    However, the Game which object you have new Constructed in this code, will be released. That object has lost its reference because the reference of the Game is again to the old Game. 

    Be aware to dimension this with Private on Global level otherwise they go out of scope.


    Success
    Cor


    Well it took a while but I got this example to work after basically putting new and public on everything. I got what you meant right away. Its like disposing an image. But, getting everything just right takes a while.

    Now off to try the method in the big spaghetti plate I am working with. :)

    Thanks Cor.

    I will work with the copy part too Leshay which is another way?

    Here is a working example for future generations.

    First form set value to 3, click open:

    Public Class Form1
    
        Private game1 As New Game
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            Game1.value = 3
            Label1.Text = game1.value.ToString
        End Sub
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Using gm As New Form2(game1)
                gm.ShowDialog()
                game1 = gm.OriginalGame
            End Using
    
            Label1.Text = game1.value.ToString
    
        End Sub
    End Class

    Second dialog form value shows 3 when opened. Change it to 5.

    Public Class Form2
        Public BackupGame As New Game
        Public OriginalGame As New Game
    
        Public Sub New(OriginalGame As Game)
    
            ' This call is required by the designer.
            InitializeComponent()
    
            ' Add any initialization after the InitializeComponent() call.
            BackupGame = OriginalGame
    
            OriginalGame = New Game
            OriginalGame = BackupGame
    
            NumericUpDown1.Value = OriginalGame.value
        End Sub
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            'ok 
    
            Close()
        End Sub
    
        Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
            'cancel
            OriginalGame = BackupGame
            Close()
        End Sub
    
        Private Sub NumericUpDown1_ValueChanged(sender As Object, e As EventArgs) Handles NumericUpDown1.ValueChanged
            OriginalGame.value = NumericUpDown1.Value
        End Sub
    End Class

    Public Class Game
        Public value As Integer
    
    End Class


    Now Second form click OK and get then changed value 5 returned. Or click cancel and the original value 3 is returned.

    PS I think this could be cleaned up a bit if someone wants to make all th publics and new just right?


    Tuesday, May 9, 2017 6:27 PM
  • LOL.

    You know Cor, I could not get the example to work unless I added the

       game1 = gm.OriginalGame

    after showdialog in form1.

    So I got thinking and sure enough one can remove the part

      OriginalGame = New Game
      OriginalGame = BackupGame

    from the sub new in form2. See how I comment that out in the code below.

    So hmmm....

       Public Sub New(OriginalGame As Game)
    
            ' This call is required by the designer.
            InitializeComponent()
    
            ' Add any initialization after the InitializeComponent() call.
    
            BackupGame = OriginalGame
    
            'OriginalGame = New Game
            'OriginalGame = BackupGame
    
            NumericUpDown1.Value = OriginalGame.value
        End Sub


    PS I guess game1 = gm.OriginalGame breaks the original ref and now the copy gamebackup becomes valid or something like that there?
    Tuesday, May 9, 2017 7:27 PM
  • Hi Tommy,

    You could do the cloning (deep copy) and that would work, but I'm not sure you want to duplicate everything... does your dialog allow making changes to every member of the Game class, or just certain ones?  If it is just a subset of the properties on Game, and there aren't other places where you want this kind of "change rejection" functionality, then you might be best to just stick with writing little methods to get and set the values between the original game class instance and a set of associated fields in the dialog.

    If you can change everything in the Game class from the dialog, or if there are other places where you need the change tracking, then you might consider just designing the class to support change tracking through the IChangeTracking interface.

    Here's a little example which handles any value type properties when you add associated fields suffixed with the word "Original".

    Public Class Game
        Implements System.ComponentModel.IChangeTracking
    
        Public Property GridSize As Size
        Private _GridSizeOriginal As Size
        Public Property MapOriginPt As Point
        Private _MapOriginPtOriginal As Point
        Public Property ScaleFactor As Single
        Private _ScaleFactorOriginal As Single
    
        Shared comparisons As New List(Of Func(Of Game, Boolean))
        Shared updaters As New List(Of Action(Of Game))
        Shared Sub New()
            For Each originalField In (From field In GetType(Game).GetFields(Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic) Where field.Name.EndsWith("Original"))
                Dim currentField = GetType(Game).GetField(originalField.Name.Substring(0, originalField.Name.Length - 8),
                                                     Reflection.BindingFlags.Instance Or Reflection.BindingFlags.NonPublic)
                comparisons.Add(Function(g As Game)
                                    Return originalField.GetValue(g).Equals(currentField.GetValue(g))
                                End Function)
                updaters.Add(Sub(g As Game)
                                 originalField.SetValue(g, currentField.GetValue(g))
                             End Sub)
            Next
        End Sub
    
        Public ReadOnly Property IsChanged As Boolean Implements IChangeTracking.IsChanged
            Get
                For Each comparer In Game.comparisons
                    If Not comparer(Me) Then Return True
                Next
                Return False
            End Get
        End Property
    
        Public Sub AcceptChanges() Implements IChangeTracking.AcceptChanges
            For Each updater In Game.updaters
                updater(Me)
            Next
        End Sub
    End Class

    Now then, for the collection properties you should design each collection so that it supports change tracking internally and then you can expand the code for IsChanged to check the state of each collection (as well as AcceptChanges to accept each collections changes).

    For reference type properties such as the Bitmap you would need to define a way of determining what constitutes a change.  You might follow the normal INotifyPropertyChanged implementation and just hold a flag to indicate a changed state when the property value is set.  Or you might have a custom object which itself implements IChangeTracking.

    I'm not convinced that this is the correct route to go for this particular need, but I figured I'd at least mention the option.


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Tuesday, May 9, 2017 7:28 PM
    Moderator
  • Tommy, 

    I'm always fighting with that ShowDialog, however here a sample what I mean. 

    Public Class Form1
        Private ThisTommy As New Tommy("")
        Private OldTommy As Tommy
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            ThisTommy.Text = TextBox1.Text
            OldTommy = ThisTommy
            Using ofd As New Form2(ThisTommy)
                ofd.TextBox1.Text = ThisTommy.Text 'a little bit dirty for this sample
                If ofd.ShowDialog <> DialogResult.OK Then
                    ThisTommy = OldTommy
                End If
                TextBox1.Text = ThisTommy.Text
            End Using
        End Sub
    End Class
    Public Class Tommy
        Public Property Text As String
        Public Sub New(Text As String)
            Me.Text = Text
        End Sub
    End Class
    Public Class Form2
        Friend Property ThisTommy As Tommy
        Public Sub New(ThisTommy As Tommy)
            InitializeComponent()
            Me.ThisTommy = ThisTommy
            TextBox1.Text = ThisTommy.Text
        End Sub
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            DialogResult = DialogResult.OK
            Me.Close()
        End Sub
        Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles TextBox1.TextChanged
            ThisTommy.Text = TextBox1.Text
        End Sub
    End Class


    Success
    Cor

    Cor,

    I missed this post for a while sorry.

    I see. You are making the copy in the main form I made it in the dialog form. That was what I was missing was referencing the dialog form variables back in form1 after the dialog closes. At first I was trying to pass the class back through the original ref in the sub new.

    Tuesday, May 9, 2017 8:26 PM
  • Hi Reed!

    That looks fun. :)

    Yes this dialog just allows changes of a few things in the class.

    The dialog is a tile editor for the main form which is a game/map editor and running game. Its just the continuation of the tank game from the other threads and other we have discussed in the past. Got into learning what the tile games are and just drawing with tiles in general I mean its very old and has math and interests the artist in me too etc.

    I just recall doing this same dialog cancel thing over and over in my last lifetime and now that I know what a class is better I am trying to use them. I am just trying to boil it down to the basic simple "proper way" for my normal use to and from dialogs.  But whether class or array the same thing happens as I recall.

    From the past I did have cases like you describe I can sort of follow what you mean.

    For sure my current game class could be set up better. This adventure is still in the design build stage and thats most likely as far as it will go in this form if ever.

    Tuesday, May 9, 2017 8:29 PM
  • PS Cant resist showing...

    Tuesday, May 9, 2017 8:37 PM
  • That being the case I would just copy the relevant values from the class into local variables in the dialog.  Have the dialog edit the local variables, then copy their values back to the class when/if the user clicks OK.

    Very nice looking interface!  However, I am a bit concerned that your tile sheet came from RPG Maker resources (they look awful similar!) in which case you couldn't use them for a non-RPG Maker game engine.  (well, of course you can make it for your own personal use - you just cant distribute it in any way)


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Tuesday, May 9, 2017 10:46 PM
    Moderator
  • That being the case I would just copy the relevant values from the class into local variables in the dialog.  Have the dialog edit the local variables, then copy their values back to the class when/if the user clicks OK.

    Very nice looking interface!  However, I am a bit concerned that your tile sheet came from RPG Maker resources (they look awful similar!) in which case you couldn't use them for a non-RPG Maker game engine.  (well, of course you can make it for your own personal use - you just cant distribute it in any way)


    Reed Kimble - "When you do things right, people won't be sure you've done anything at all"

    Yes and the whole background and all of that. Not the sprites though! Those are (c) me.

    So I ended up needing to use the Leshay ICloneable to get a clean copy with my big class.

    I tried the exact same as the small example and etc but only the ICloneable seems to work with my larger class. Of course maybe I missed something.

        Public Game1 As New Game
        Public GameCopy As New Game
    
        Private Sub TileEditorBtn_Click(sender As Object, e As EventArgs) Handles TileEditorBtn.Click
    
            GameCopy = DirectCast(Game1.Clone, Game)
    
            Using te As New TileEditor(Game1)
    
                If te.ShowDialog = DialogResult.OK Then
                    Game1 = te.thisGame1
                Else
                    Game1 = GameCopy
                End If
    
            End Using
        End Sub
    

    • Edited by tommytwotrain Tuesday, May 9, 2017 11:51 PM optimized
    Tuesday, May 9, 2017 11:46 PM