Monday, December 31, 2012

Passing Data Between Forms Redux

Almost exactly two years ago, I published the Passing Data Between Forms post. I have had numerous questions and comments about that post and now I’ve decided that it deserves an additional example.

In that post, I had an example (the 3rd one) that utilized Interfaces. In the example, we had a MainForm that passed a DataSet to Form1. Form1 then allowed the user to make some changes to the data in that DataSet and then we wanted to have MainForm automatically reflect those changes ... even while the user was still working in Form1. An interface worked well for that scenario. At the time, I wanted to keep the post simple so as not to confuse beginners. But, I really need to expand on that example and show how to accomplish this task using delegates/events.

So, without further ado (pun intended), here it is:

First, let's take a look at Form1. Minimally, you can implement this whole thing with a simple event, which uses standard EventArgs, as follows:

public class Form1 : Form
{
    public event EventHandler DataChanged;
    private CustomerDataSet oData;

    public Form1(CustomerDataSet dsCust)
    {
        this.oData = dsCust;
    }
    public void DoStuff()
    {
        // code to do stuff with this.oData
        // ...
        // and then fire the event if anyone has subscribed
        this.OnDataChanged(new EventArgs());
    }
    private void OnDataChanged(EventArgs e)
    {
        if (this.DataChanged != null)
            this.DataChanged(this, e);
    }
}

Then, the MainForm looks like this:

public class MainForm : Form
{
    private CustomerDataSet dsCustomer;

    // ...

    // then code elsewhere to instantiate and fill your data
    this.dsCustomer = new CustomerDataSet();
    // plus maybe other code to fill the dataset


    // ...

    // code to instantiate Form1, pass it the DataSet, handle the event
    Form1 oForm = new Form1(this.dsCustomer);
    oForm.DataChanged += new EventHandler(DataChanged);
    oForm.Show();

    public void DataChanged(object sender, EventArgs e)
    {
        // code here to do stuff with this.dsCustomer
    }
}

In this particular case, we don't really need EventArgs. Because we passed the DataSet into Form1 to begin with, we already know everything we need to know, namely the data that has changed will be the same DataSet that we passed into Form1. So, since we don’t need any EventArgs, we could create a custom delegate to use as our event handler. We will change our two forms like this:

Form1:

public class Form1 : Form
{
    public delegate void DataChangedEventHandler();
    public DataChangedEventHandler DataChanged;
    private CustomerDataSet oData;

    public Form1(CustomerDataSet dsCust)
    {
        this.oData = dsCust;
    }
    public void DoStuff()
    {
        // code to do stuff with this.oData
        // ...
        // and then fire the event if anyone has subscribed
        this.OnDataChanged();
    }
    private void OnDataChanged()
    {
        if (this.DataChanged != null)
            this.DataChanged();
    }
}

The only difference in MainForm is that the event handler doesn't need the usual parameters (object sender, EventArgs e), so change the DataChanged event handler to look like this:

// code to instantiate Form1, pass it the DataSet, handle the event
Form1 oForm = new Form1(this.dsCustomer);
oForm.DataChanged += new Form1.DataChangedEventHandler(DataChanged);
oForm.Show();

public void DataChanged()
{
    // code here to do stuff with this.dsCustomer
}

Another way of handling the event in the MainForm is to not even bother with the above DataChanged() method and use an anonymous delegate instead, so you could change the MainForm code to look like this:

// code to instantiate Form1, pass it the DataSet, handle the event
Form1 oForm = new Form1(this.dsCustomer);
oForm.DataChanged += delegate
{ 
    // code here to do stuff with this.dsCustomer
};
oForm.Show();

There are other things you can do when creating your own delegates and/or events and, in fact, I have a written a blog post about a DataAccess class that does some interesting things with anonymous delegates: DataAccess - Part III.

However, expanding on ideas for delegates and/or events is beyond the scope of this simple post. Perhaps in the future I’ll write something more extensive, but in the meantime, you can always start off with an MSDN article such as this one: Handling And Raising Events

11 comments:

  1. Hi, i begin in c# development and my question is what for library have i to include to use CustomerDataSet type? Thanks

    ReplyDelete
    Replies
    1. Hi!

      CustomerDataSet is just an example of a Typed DataSet. You can create your own Typed DataSets using an .xsd An .xsd is a file containing xml that describes a schema for data. It can be used to describe the schema for a DataSet as well. If you need to know more about what an .xsd is, you should be able to find some decent articles by Googling ".xsd DataSet"..

      I have a few blog posts on the subject of Typed DataSets.

      http://geek-goddess-bonnie.blogspot.com/2010/04/create-xsd.html
      http://geek-goddess-bonnie.blogspot.com/2009/09/tableadapters-are-crap.html
      and maybe this one too:
      http://geek-goddess-bonnie.blogspot.com/2012/08/msdatasetgenerator-gone-wild.html

      I hope that helps ...

      Delete
  2. Hi Bonnie,
    If in the MainForm positioned at Record=3 (example), and if passing CustomerDataset to Form1 , how will know (example) TxtBoxCustomerId (and other txtbox controls) from Form1 , need to load the value of Record 3 table Customer.CustomerId , Customeername...?
    Where Bindings txtBoxCustomerId (and other txtboxs) from Form1?
    Please example
    Thanks and best regards

    ReplyDelete
  3. Hi, Bonnie
    The problem, is that when he passes a dataset to a child form (in this example, form1), it doesn't keep any info on what is the current record, so he has to apply a filter or .find() or ... to show that record in the child form. Is there an easy and elegant way to do this?
    How binding textbox and other control in child form (form1) when passing dataset parameter - please example?
    Bonnie, please help me
    Thanks and best regards
    Vojislav

    ReplyDelete
  4. Hi Bonnie, I am relatively new to C# I have a problem passing variables from my MainProgram from to a secondary form (Tasks), and I can't figure out what the issue is.

    namespace WindowsFormsApplication1
    {
    public partial class MainProgram : Form
    {
    string datafilePath;

    Tasks taskForm = new Tasks();

    public void LoadConfigFile()
    {
    // This where I read the cfg file that contains the datafilePath
    // I also load variables that control the attributes of the taskForm
    // This class opens the file and reads the line below.
    dataFilePath = objReader2.ReadLine(); /* READ DATA FILES PATH */
    }

    public void LoadScreen()
    {
    //The taskForm is one form of an MDI which I load and place on the screen here
    //the location and other attributes are based on some other information from the
    //cfg file. All of this works!
    //if the user has a certain variable set in the cfg file then it starts with
    //the taskForm showing.
    {
    taskForm.Show();
    }
    }

    public MainProgram()
    {
    InitializeComponent();
    LoadConfigFile();
    //I want to pass the dataFilePath that is loaded from the cfg file to the
    //taskForm so that the program knows where to find the user definable .CSV
    //file that will load up in the taskForm.
    LoadScreen();
    }

    //Then all the button presses and menu functions are here.
    }
    }

    Everything works as long as I set the path in the taskForm, but how do i pass the data from the cfg file loaded in MainProgram Form to the taskForm. I can't use a constructor because I don't have the variables filled at the time that I need to create the MDI forms.

    Thanks,
    Doug

    ReplyDelete
    Replies
    1. Hi Doug, If you're loading taskForm in the LoadScreen() method (and by "loading" I mean instantiating it: this.taskForm = new TaskForm(); ...), then you already *do* have the datafilePath, since the constructor of the MainProgram calls LoadConfigFile() before it calls LoadScreen(). So, I don't see why you couldn't use the constructor of taskForm to pass the datafilePath variable.

      However, if I'm wrong about where you're instantiating taskForm, then another thing you could do is set a public property in your TaskForm class, like this:

      public string DataFilePath { get; set; }

      And then set that property in the LoadScreen() method, like this:

      public void LoadScreen()
      {
      //The taskForm is one form of an MDI which I load and place on the screen here
      //the location and other attributes are based on some other information from the
      //cfg file. All of this works!
      //if the user has a certain variable set in the cfg file then it starts with
      //the taskForm showing.
      {
      taskForm.DataFilePath = datafilePath;
      taskForm.Show();
      }
      }

      Will that work for you?

      Delete
  5. Hi Bonnie, thank you so much for your quick response! The problem I had with the first possibility is that taskForm needs to be global throughout the Main Program form as there are many classes that change attributes with that form throughout the program. If I put it in the LoadScreen class then I can't access taskForm from the other classes. Unless there is a way?

    I tried the second option using the {get; set;} and everything appears to work....the program compiles and runs, but the data doesn't get passed - it just ends up with a null value??

    Doug

    ReplyDelete
  6. Hi Bonnie,

    I think I have figured it out. Thank you for your help.

    I created a class in the 2nd form (Tasks) that is called PostConstructor and I call that Class from the Main Program as:

    taskForm.PostConstuctor(dataFilePath);

    Then I show the form from the Main Program in the LoadScreen class as part of loading the MDI and it loads - finding the user set path to the datafile that it loaded in MainProgram from the cfg file.

    It is basically the second way that you recommended only I am using a class instead of a string. Is this considered an acceptable way of doing this?

    Thanks again,
    Doug


    ReplyDelete
    Replies
    1. Hi Doug,

      Sorry for not replying to your earlier question. I saw your question, but I was busy with work at the time ... and then I forgot about you. Sorry!!!

      I think that what you are calling a "class", PostConstructor, is actually a method. You might have your terminology mixed up a bit. And I still don't think you need it.

      I'm sorry, in going back and looking at your original code, I see that I missed the fact that you're instantiating taskForm right with the declaration of the taskForm variable. That's fine. My suggestion about the DataFilePath property, the {get;set;}, will still work. But since you don't want to put that in the LoadScreen (again, that's a method, not a class), then you could put it directly in the Main Program constructor, like this:

      public MainProgram()
      {
      InitializeComponent();
      LoadConfigFile();
      taskForm.DataFilePath = datafilePath;
      LoadScreen();
      }

      And you don't need the PostConstructor() method at all.

      Delete
  7. Hi Bonnie,

    I guess the problem I had with the {get;set;} property is that how can I send the data after the initial taskForm initialize to the taskForm so that the when the form open it uses the data from the path to load the file into the taskForm form?

    This is what I did now:

    namespace WindowsFormsApplication1
    {
    public partial class Tasks : Form
    {
    DataGridView loadTasksView = new DataGridView();
    DataTable loadTasksTable = new DataTable();
    string tasksPath;
    //public string dataFilePath { get; set; }


    public void TasksLoad()
    {
    this.Size = new Size(750, 500);
    loadTasksView.Size = new Size(300, 400);
    loadTasksView.Location = new Point(5, 5);
    string[] raw_text = System.IO.File.ReadAllLines(@tasksPath + @"Tasks.csv");
    string[] data_col = null;
    int x = 0;
    foreach (string text_line in raw_text)
    {
    //MessageBox.Show(text_line);
    data_col = text_line.Split(',');
    if (x == 0)
    {
    //header
    for (int i = 0; i <= data_col.Count() - 1; i++)
    {
    loadTasksTable.Columns.Add(data_col[i]);
    }
    x++;
    }
    else
    {
    //data
    loadTasksTable.Rows.Add(data_col);
    }
    }
    loadTasksView.DataSource = loadTasksTable;
    this.Controls.Add(loadTasksView);
    }



    public Tasks()
    {
    InitializeComponent();
    }


    public void Postconstructor(string dataFilePath)
    {
    if (dataFilePath != null)
    {
    tasksPath = dataFilePath;
    TasksLoad();
    }
    }
    }
    }

    Thank you so much for all of your help with this!

    Doug

    ReplyDelete
    Replies
    1. Hi Doug,

      Ah, OK, I see what you're trying to do now. Instead of just a plain old get/set for the dataFilePath property (which you commented out for now), what you could have done instead of having a method (PostConstructor) to run some code, is to use a property with a backing variable. What this allows you to do, is make the property execute that other code in the getter or the setter. So, for example, you could have something like this in your Tasks Form:

      // the "m_" is just a convention I like to use for backing variables
      private string m_dataFilePath = null;
      public string dataFilePath
      {
      get { return this.m_dataFilePath; }
      set
      {
      if (value != null)
      {
      this.m_dataFilePath = value;
      this.TasksLoad();
      }
      }
      }

      Then, you simply set the property in your MainProgram, like I previously wrote (taskForm.dataFilePath = datafilePath;) and you won't need to call the PostConstructor() method. The reason for doing it this way makes more sense if you think about it. You're setting a Task form property. The MainProgram doesn't need to know anything about what happens when you set that property, it just knows it has to set it. Now, if you needed to more stuff and set a few more properties, then perhaps you'd have a method to do that. But, I wouldn't call that method PostConstructor() ... that doesn't tell you anything about what it does. I would call that method something like SetupAdditionalConfiguration(), and you'd have a bunch of parameters that you'd pass to it.

      (Note that your TasksLoad() method would have to use dataFilePath instead of tasksPath).

      Note my use of "this." ... you should get in the habit of using "this." for accessing anything "global" to the class, like properties, variables and methods. Any variable that is local to a method (defined in and used only in the method) is not (and cannot) be prefixed with "this." ... it makes it obvious to anyone reading the code (including you, months later) which variables are local to a method.

      What you have is working for you (and I'm glad you figured it out!!!) ... if you're happy with it, then keep it that way. I'm just giving you a few more pointers about how to write better code. Keep it in mind for next time (or refactor your existing code with my suggestions if you wish).

      I'm glad I could help! Happy coding!

      Delete