Передача данных между формами в C#
Данные между формами в Windows Forms передаются через конструктор, публичные свойства, события, делегаты или общий объект модели. Прямой доступ к чужим текстовым полям работает, но связывает окна намертво и усложняет поддержку. Ниже собраны рабочие способы с кодом для .NET 8, сравнение подходов и разбор типичных ошибок.

Содержание
Основная идея: форма - это обычный класс
Каждое окно в Windows Forms представляет собой класс, унаследованный от Form. Значит, всё, что верно для классов, верно и для форм: у них есть конструкторы, свойства, методы и события, а один экземпляр может хранить ссылку на другой. Новички часто спрашивают, как "достать" значение из textBox1 другой формы. Технически это возможно после изменения модификатора элемента на public, но лучше передавать само значение, а не доступ к элементу интерфейса.
Допустим, есть главная форма MainForm и вторая форма EditForm. Нужно передать имя пользователя во вторую форму, отредактировать его и вернуть обратно. Эту задачу решим разными способами и в конце сравним их.
Передача через конструктор
Самый прозрачный путь для данных, которые нужны форме сразу при открытии. Добавьте в дочернюю форму конструктор с параметрами и не забудьте вызвать InitializeComponent(), иначе элементы не создадутся и вы получите NullReferenceException. Подробности об этой ошибке есть в статье про NullReferenceException.
public partial class EditForm : Form
{
public EditForm(string userName)
{
InitializeComponent();
textBoxName.Text = userName;
}
}
// MainForm
private void buttonEdit_Click(object sender, EventArgs e)
{
var form = new EditForm(labelName.Text);
form.Show();
}Метод конструктора передаёт данные в одну сторону: из главной формы в дочернюю. Для возврата результата понадобится один из следующих приёмов. Если у формы остаётся пустой конструктор для дизайнера, учтите, что дизайнер Visual Studio требует публичного конструктора без параметров только для отображения формы в редакторе, при запуске программы он не используется.
Свойства и ShowDialog
Когда дочернее окно служит диалогом, вызывайте его через ShowDialog(). Метод блокирует вызывающую форму до закрытия диалога и возвращает значение DialogResult. Данные забираются из публичного свойства, которое читает значение из поля ввода. Элементы интерфейса остаются закрытыми, наружу выходит только то, что нужно.
public partial class EditForm : Form
{
public string UserName
{
get => textBoxName.Text.Trim();
set => textBoxName.Text = value;
}
public EditForm()
{
InitializeComponent();
buttonOk.DialogResult = DialogResult.OK;
buttonCancel.DialogResult = DialogResult.Cancel;
AcceptButton = buttonOk;
CancelButton = buttonCancel;
}
}
// MainForm
private void buttonEdit_Click(object sender, EventArgs e)
{
using var form = new EditForm { UserName = labelName.Text };
if (form.ShowDialog(this) == DialogResult.OK)
{
labelName.Text = form.UserName;
}
}Обратите внимание на using. Форма, показанная через ShowDialog, не освобождается при закрытии, а лишь скрывается. Если оставить её без Dispose, дескрипторы окон будут накапливаться. Для окон, открытых методом Show(), обратное правило: они освобождаются сами при закрытии, и оборачивать их в using нельзя, иначе форма исчезнет сразу после вызова.
Свойство, которое присваивается из главной формы, подходит и для передачи данных "вперёд" после создания объекта. Свойства удобнее конструктора, когда параметров много и часть из них необязательна. Обязательное условие: присваивать значения нужно до вызова Show или ShowDialog, либо обновлять отображение вручную.
События для обратной связи
Если вторая форма должна сообщать главной об изменениях, пока остаётся открытой, используйте событие. Дочерняя форма ничего не знает о том, кто на неё подписан, поэтому её можно открывать из любого места программы. Это классический вариант слабой связанности.
public class NameChangedEventArgs : EventArgs
{
public string NewName { get; }
public NameChangedEventArgs(string newName) => NewName = newName;
}
public partial class EditForm : Form
{
public event EventHandler<NameChangedEventArgs>? NameChanged;
private void buttonApply_Click(object sender, EventArgs e)
{
NameChanged?.Invoke(this, new NameChangedEventArgs(textBoxName.Text));
}
}
// MainForm
private void buttonEdit_Click(object sender, EventArgs e)
{
var form = new EditForm();
form.NameChanged += (s, args) => labelName.Text = args.NewName;
form.Show(this);
}Обработчик в виде лямбды живёт столько же, сколько форма, поэтому утечек не возникает: после закрытия окна обе стороны становятся мусором. Если же подписка делается на долгоживущий объект, например статический класс, отписывайтесь в событии FormClosed.
Делегаты и Action
Когда не нужно полноценное событие, достаточно передать в форму метод обратного вызова. Типы Action<T> и Func<T> избавляют от объявления своего делегата. Приём подходит для одноразовых действий, например "сохранить результат". Идея передачи метода как параметра подробно разобрана в статье про делегаты и лямбды.
public partial class EditForm : Form
{
private readonly Action<string> _onSave;
public EditForm(string name, Action<string> onSave)
{
InitializeComponent();
textBoxName.Text = name;
_onSave = onSave;
}
private void buttonSave_Click(object sender, EventArgs e)
{
_onSave(textBoxName.Text);
Close();
}
}
// MainForm
new EditForm(labelName.Text, name => labelName.Text = name).Show();Общий объект данных
Когда обе формы работают с одним набором данных, например с карточкой клиента, самое чистое решение - передать им один и тот же объект модели. Форма меняет свойства объекта, а главное окно перечитывает его после закрытия диалога. Модель можно сделать реализующей INotifyPropertyChanged, и тогда привязка данных обновит все окна сама. Про этот интерфейс написано в материале о реализации INotifyPropertyChanged.
public class Customer
{
public string Name { get; set; } = "";
public string Email { get; set; } = "";
}
public partial class CustomerForm : Form
{
private readonly Customer _customer;
private readonly BindingSource _binding = new();
public CustomerForm(Customer customer)
{
InitializeComponent();
_customer = customer;
_binding.DataSource = _customer;
textBoxName.DataBindings.Add("Text", _binding, nameof(Customer.Name));
textBoxEmail.DataBindings.Add("Text", _binding, nameof(Customer.Email));
}
}Привязка через DataBindings записывает введённый текст в свойство при потере фокуса поля. Объект при этом общий, поэтому главная форма видит изменения без дополнительного кода. Если нужна возможность отмены, работайте с копией объекта и применяйте её только по кнопке OK.
Интерфейс как посредник
Если дочернее окно вызывает методы родителя, привяжите его к интерфейсу, а не к конкретной форме. Тогда одну и ту же форму можно открывать из разных окон и подменять реализацию в тестах. Интерфейс объявляет только нужные операции.
public interface INameReceiver
{
void SetName(string name);
}
public partial class MainForm : Form, INameReceiver
{
public void SetName(string name) => labelName.Text = name;
}
public partial class EditForm : Form
{
private readonly INameReceiver _receiver;
public EditForm(INameReceiver receiver)
{
InitializeComponent();
_receiver = receiver;
}
private void buttonOk_Click(object sender, EventArgs e)
=> _receiver.SetName(textBoxName.Text);
}Вызов из главной формы выглядит так: new EditForm(this).Show();. Подробнее об изменении элементов родителя из дочернего окна, включая варианты с владельцем, можно прочитать в статье изменение элемента формы из другой формы.
Способы, которых лучше избегать
Статические переменные и публичные поля с модификатором public static работают быстро, но превращают данные в глобальное состояние. Любое окно может записать туда что угодно, а отследить, кто и когда изменил значение, сложно. Допустимы статические данные только для настроек всего приложения, которые читаются многими окнами и редко меняются.
Свойство Owner и коллекция Application.OpenForms позволяют найти открытую форму по типу, но это тоже связывает код с конкретными классами. Типичный пример:
var main = Application.OpenForms.OfType<MainForm>().FirstOrDefault();
main?.SetName("Новое имя");Такой код полезен как временное решение для мелких программ, но при росте проекта лучше заменить его событием или интерфейсом. Отдельная опасность - публичные элементы управления: любой код сможет менять их свойства, и вы потеряете контроль над логикой окна.
Сравнение способов
| Способ | Направление | Подходит для | Слабая сторона |
|---|---|---|---|
| Конструктор | в дочернюю форму | начальные данные | нельзя вернуть результат |
| Свойства и ShowDialog | в обе стороны | модальные диалоги | нужна проверка DialogResult |
| События | из дочерней формы | немодальные окна, уведомления | нужно следить за отпиской |
| Action и делегаты | из дочерней формы | разовый обратный вызов | сложнее расширять |
| Общий объект | в обе стороны | редактирование сущностей | общий источник изменений |
| Интерфейс | в родителя | больших проектов, тестов | больше кода |
| Статика, OpenForms | любое | мелких программ | скрытая связанность |
Передача данных в WPF
В WPF действуют те же принципы: конструктор окна, свойства, события и общий объект. Для диалогов также используется ShowDialog(), который возвращает bool?. Общие данные принято хранить в модели представления и привязывать к ней оба окна через DataContext, поэтому передавать значения вручную приходится реже. Основы подхода рассмотрены в статье про MVVM.
Частые ошибки
- Создание нового экземпляра главной формы вместо использования существующей: данные уходят в невидимое окно, а на экране ничего не меняется. Передавайте
thisили используйте события. - Обращение к элементам до
InitializeComponent(). - Забытый
usingу модальной формы либо, наоборот,usingвокруг немодальной. - Чтение свойства после закрытия формы без проверки
DialogResult: при отмене пользователь не хотел сохранять изменения. - Хранение ссылок на закрытые формы. После закрытия вызов их методов приведёт к
ObjectDisposedException.
Частые вопросы
Как передать значение из Form1 в Form2?
Самый простой способ - параметр конструктора второй формы либо публичное свойство, которое вы присваиваете перед вызовом Show() или ShowDialog().
Как вернуть значение из Form2 в Form1?
Для модального диалога прочитайте публичное свойство формы после ShowDialog() с результатом OK. Для немодального окна объявите событие и подпишитесь на него в главной форме.
Можно ли сделать текстовое поле public и обращаться к нему напрямую?
Можно, но это плохая практика. Любое окно получит возможность изменять внутренности другого, а переименование элемента сломает несколько форм. Предпочтительнее свойство, которое возвращает нужное значение.
Чем отличаются Show и ShowDialog?
Show() открывает окно и сразу возвращает управление, пользователь может работать с обеими формами. ShowDialog() блокирует вызывающее окно и возвращает результат диалога, поэтому его используют для ввода данных.
Как передать данные между тремя и более формами?
Используйте общий объект модели, который передаётся всем окнам, или событийную шину, через которую формы обмениваются сообщениями. Цепочки "форма знает о форме" быстро превращаются в запутанный код.
Работает ли это в WPF и .NET MAUI?
Принципы те же: конструктор, свойства, события и общая модель. Но в WPF и MAUI вместо ручной передачи обычно применяют привязку данных к модели представления.





