1

I've been spending some time learning about Delegates in C# and have wired up a small Windows Form app containing two forms to test them out.

My goal is to have the AddContactForm push new Contact data back to the ContactsForm using an EventHandler delegate. I currently have this working with the code below, however ultimately I'd like to make ContactAddedHandler in the AddContactForm private, and pass in a function to the AddContactForm constructor, which is used to subscribe to the delegate like so:

public AddContactForm(SomeSubscriberFunction foo)
{
    ContactAddedHandler += foo;
}

I've tried making SomeSubscriberFunction various permutations of the following without success:

  • Func<object?, AddContactEventArgs?>
  • Func<AddContactEventArgs?>
  • Action<object?, AddContactEventArgs?>

If anyone could provide any guidance on the following two questions, it would be greatly appreciated:

#1. Am I approaching this problem in a completely non-best practice way, if so what is the correct approach.

#2. If this approach using delegates is correct, how can I achieve what I am attempting to do?

Thank you.

ContactsForm

public void addContactBtn_Click(Object sender, EventArgs e)
{
    AddContactForm addContactForm = new AddContactForm();
    addContactForm.ContactAddedHandler += ContactAdded;
    addContactForm.Show();
}

public void ContactAdded(object? sender, AddContactForm.AddContactEventArgs e)
{
    Console.Write("Contact Added");
}

AddContactForm

 public partial class AddContactForm : Form
 {

     public class AddContactEventArgs(Contact contact) : EventArgs 
     {
         public Contact contact = contact;
     }

     public event EventHandler<AddContactEventArgs>? ContactAddedHandler;

     public AddContactForm()
     {
         InitializeComponent();
     }

     private void saveContactBtn_Click(object sender, EventArgs e)
     {
         Contact c = new Contact("Test");
         AddContactEventArgs args = new AddContactEventArgs(c);
         if (ContactAddedHandler != null)
         {
             ContactAddedHandler(this, args);
         }
     }
 }
2
  • 1
    If there's only one subscriber, you don't have to use an event. Just store the delegate itself. Commented Dec 1, 2023 at 21:53
  • Even if there are multiple subscribers, a delegate can contain references to multiple methods (just like an event does). When they do, they're called multicast delegates and they basically work just like an event at that point, including being able to .Invoke/BeginInvoke them. Commented Dec 1, 2023 at 22:30

2 Answers 2

2

Action delegates are quite often used to generate notifications in similar scenarios. You already have a Contact class object, that's the object that stores the required information, so you most probably don't need a custom EventArgs object to wrap the same data.

For example, if the Action delegate notification is restricted to only the code that creates the Form class:

Note: written for .NET 6+, since you had the windows-forms-core tag

public partial class ContactForm : Form {
    public ContactForm() => InitializeComponent();

    private void AddContactBtn_Click(object sender, EventArgs e) {
        Action<Contact> contactAdded = (contact) => Debug.WriteLine(contact.Name);
        AddContactForm addContact = new(contactAdded);
        addContact.ShowDialog();
    }
}

If the Action delegate can only be injected via the Form's Constructor, as shown in this post, you can simply assign the delegate to a private Field and invoke it when needed:

public partial class AddContactForm : Form {
    Action<Contact>? NotifyNewContact = null;
    public AddContactForm() => InitializeComponent();
    public AddContactForm(Action<Contact> newContact) : this() => NotifyNewContact = newContact;

    private void SaveContactBtn_Click(object sender, EventArgs e) {
        NotifyNewContact?.Invoke(new("Test"));
    }
}

You could also make the Action accessible from the outside and add more than one subscriber.
You can use a multicast delegate as it was used before. But better create a level of indirection, adding the event keyword, so add() and remove() methods are created under the hood, to protect the object.

You can do this, in the Constructor of AddContactForm:

public AddContactForm(Action<Contact> newContact) : this() => NotifyNewContact = newContact;
// Instead of NotifyNewContact += newContact

but not this, somewhere else:

addContact.NotifyNewContact = (c) => Debug.WriteLine(c.Name);
// Instead of += (c) => [...];

public partial class AddContactForm : Form {
    protected internal event Action<Contact>? NotifyNewContact = null;
    public AddContactForm() => InitializeComponent();
    public AddContactForm(Action<Contact> newContact) : this() => NotifyNewContact += newContact;

    private void SaveContactBtn_Click(object sender, EventArgs e) {
        NotifyNewContact?.Invoke(new("Test"));
    }
}

You can then add multiple delegates, where the instance of AddContactForm is visible, to anything else that can pass an Action delegate or in many different other ways:

public partial class ContactForm : Form {
    // [...]

    private void AddContactBtn_Click(object sender, EventArgs e) {
        Action<Contact> contactAdded = (contact) => Debug.WriteLine(contact.Name);
        AddContactForm addContact = new(contactAdded);

        // Subscribe a second time, just a lambda
        addContact.NotifyNewContact += (c) => Debug.WriteLine(c.Name);

        // Subscribe using a delegate that something else provided
        // Assume otherContact is coming from elsewhere
        Action<Contact> otherContact = new((c)=> Debug.WriteLine(c.Name));
        addContact.NotifyNewContact += otherContact;

        // Subscribe using a method that matches the signature
        addContact.NotifyNewContact += ProcessContact;
       
        // [...]
    }

   // [...]
    private void ProcessContact(Contact contact) {
        Debug.WriteLine(contact.Name);
    }
}
1
  • 1
    Thank you for the detailed writeup. This works well and is much simpler than the approach I was attempting. Commented Dec 4, 2023 at 15:26
2

This is a bit tricky. Your question starts out as being about learning to use delegates, and Jimi's answer has given that a good treatment [▲]. But, your post (as it reads today) states that your primary concern is:

#1. Am I approaching this problem in a completely non-best practice way?

The aim of this answer is to offer a perspective that using delegates, as a way to accomplish the goal of making the ContactsForm aware of a new result from AddContactsForm (the idea of having to "push" the result), adds a layer of complexity that isn't really necessary because of the way the ShowDialog method is designed to work in the first place.


Built to the Task

The ShowDialog() method is already well-suited for what you're doing. Unlike the Show method, the Form.Handle does not dispose as a result of closing the dialog, so you can show it repeatedly without consequence. To use it this way, you would make it a member of your MainForm class, and wait for the MainForm.Disposed event to dispose its Handle.

Since AddOrEditContactForm keeps its state, no event is required. Just check whether the operation was accepted or canceled, and inspect the Contact property if it's known to be valid.

add contact work flow

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        Disposed += (sender, e) =>AddOrEditContactForm.Dispose();
        buttonAddContact.Click += (sender, e) =>
        {
            switch (AddOrEditContactForm.ShowDialog(this))
            {
                case DialogResult.OK: 
                    MessageBox.Show($"Added: {AddOrEditContactForm.Contact}"); 
                    break;
                case DialogResult.Cancel: MessageBox.Show("Cancelled!"); break;
            }
        };
        buttonEditContact.Click += (sender, e) =>
        {
            var mockEditContact = new Contact { Name = "Tommy IV" };
            switch (AddOrEditContactForm.ShowDialog(this, mockEditContact))
            {
                case DialogResult.OK:
                    MessageBox.Show($"Edited: {AddOrEditContactForm.Contact}");
                    break;
                case DialogResult.Cancel: MessageBox.Show("Cancelled!"); break;
            }                
        };
    }
    AddOrEditContactForm AddOrEditContactForm { get; } = new AddOrEditContactForm();
}

Then, to optionally make this a multi-purpose Add or Edit form, just overload the ShowDialog to accept a Contact to preload, and of none is provided clear all the controls to give the user a fresh start.

public partial class AddOrEditContactForm : Form
{
    public AddOrEditContactForm()
    {
        InitializeComponent();
        StartPosition = FormStartPosition.CenterParent;
        CancelButton = buttonCancel;
        AcceptButton = buttonOK;
        buttonOK.Enabled = false;
        buttonOK.Click += (sender, e) =>
        {
            Contact = new Contact { Name = textBoxName.Text };
            DialogResult = DialogResult.OK;
        };
        buttonCancel.Click += (sender, e) => DialogResult = DialogResult.Cancel;
        textBoxName.TextChanged += (sender, e) => validateForm();
    }
    private void validateForm()
    {
        if( !string.IsNullOrEmpty(textBoxName.Text)) // && textBoxEmail.Text.IsValidEmail() etc...
        {
            buttonOK.Enabled = true;
        }
        else  buttonOK.Enabled = false;
    }
    public Contact? Contact { get; private set; }
    /// <summary>
    /// Make a new contact, or edit an existing one.
    /// </summary>
    public DialogResult ShowDialog(Form owner, object? args = null)
    {
        Owner = owner;
        if (args is Contact contact)
        {
            Text = "Edit Contact";
            textBoxName.Text = contact.Name;
        }
        else localClearForm();
        return base.ShowDialog(owner);
            
        void localClearForm()
        {
            Text = "Add Contact";
            foreach (var textBox in Controls.OfType<TextBox>()) textBox.Clear();
            foreach (var combobox in Controls.OfType<ComboBox>()) combobox.SelectedIndex = -1;
        }
    }
}

Mock Contact
public class Contact
{
    public string? Name { get; set; }
    public override string ToString() => $"Name: {Name}";
}
1
  • Thanks for bringing the ShowDialog functionality to my attention. Very similar to how this type of thing works on Android. Commented Dec 4, 2023 at 16:27

Not the answer you're looking for? Browse other questions tagged or ask your own question.