С# привязка к полям вложенного объекта

Кажется, я не могу найти простого и конкретного объяснения того, как привязать элементы управления в приложении WinForms к вложенным объектам с помощью привязки данных. Например:

class MyObject : INotifyPropertyChanged
{
    private string _Name;
    public string Name 
    { 
        get { return _Name; } 
        set 
        { 
            _Name = value; 
            OnPropertyChanged("Name"); 
        }    
    }

    private MyInner _Inner;
    public MyInner Inner 
    { 
       get { return _Inner; } 
       set 
       { 
           _Inner = value; 
           OnPropertyChanged("Inner"); 
       } 
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

class MyInner : INotifyPropertyChanged
{
    private string _SomeValue;
    public string SomeValue 
    {
        get { return _SomeValue; } 
        set 
        { 
            _SomeValue = value; 
            OnPropertyChanged("SomeValue"); 
        } 
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Теперь представьте себе форму с двумя текстовыми полями, первое для Name и второе для Inner.SomeValue. Я легко могу получить привязку для работы с именем, но Inner.SomeValue ненадежный. Если я заполняю объект, а затем настраиваю привязку, он показывает Inner.SomeValue в текстовом поле, но я не могу его редактировать. Если я начну с нового объекта без инициализации Inner, я не смогу заставить данные вставляться в Inner.SomeValue.

Я проверил весь MSDN, весь StackOverflow и десятки поисковых запросов с разными ключевыми словами. Все хотят говорить о привязке к базам данных или DataGrid, и большинство примеров написано на XAML.

Обновление: я попробовал полную тестовую систему Марка и добился частичного успеха. Если я нажму "все изменить!" кнопку, я, кажется, могу писать обратно во внутренний объект. Однако, начиная с MyObject.Inner null, он не знает, как создать внутренний объект. Я думаю, что на данный момент я могу обойти это, просто убедившись, что мои внутренние ссылки всегда установлены на действительный объект. Тем не менее, я не могу отделаться от ощущения, что я что-то упускаю :)


person Matt Cooper    schedule 20.10.2010    source источник


Ответы (1)


Хм - отличный вопрос; Я много раз привязывал данные к объектам и готов поклясться, что то, что вы делаете, должно работать; но на самом деле он очень неохотно замечает изменение внутреннего объекта. Мне удалось заставить его работать:

var outer = new BindingSource { DataSource = myObject };
var inner = new BindingSource(outer, "Inner");
txtName.DataBindings.Add("Text", outer, "Name");
txtSomeValue.DataBindings.Add("Text", inner, "SomeValue");

Не идеально, но работает. Кстати; вам могут пригодиться следующие служебные методы:

public static class EventUtils {
    public static void SafeInvoke(this EventHandler handler, object sender) {
        if(handler != null) handler(sender, EventArgs.Empty);
    }
    public static void SafeInvoke(this PropertyChangedEventHandler handler,
               object sender, string propertyName) {
        if(handler != null) handler(sender,
               new PropertyChangedEventArgs(propertyName));
    }
}

Тогда вы можете иметь:

class MyObject : INotifyPropertyChanged
{
    private string _Name;
    public string Name { get { return _Name; } set {
        _Name = value; PropertyChanged.SafeInvoke(this,"Name"); } }
    private MyInner _Inner;
    public MyInner Inner { get { return _Inner; } set {
        _Inner = value; PropertyChanged.SafeInvoke(this,"Inner"); } }
    public event PropertyChangedEventHandler PropertyChanged;
}

class MyInner : INotifyPropertyChanged
{
    private string _SomeValue;
    public string SomeValue { get { return _SomeValue; } set {
        _SomeValue = value; PropertyChanged.SafeInvoke(this, "SomeValue"); } }
    public event PropertyChangedEventHandler PropertyChanged;
}

И вдобавок он исправляет (небольшой) шанс нулевого исключения (состояние гонки).


Полная тестовая установка, чтобы сгладить перегибы (из комментариев):

using System;
using System.ComponentModel;
using System.Windows.Forms;
public static class EventUtils {
    public static void SafeInvoke(this PropertyChangedEventHandler handler, object sender, string propertyName) {
        if(handler != null) handler(sender, new PropertyChangedEventArgs(propertyName));
    }
}
class MyObject : INotifyPropertyChanged
{
    private string _Name;
    public string Name { get { return _Name; } set { _Name = value; PropertyChanged.SafeInvoke(this,"Name"); } }
    private MyInner _Inner;
    public MyInner Inner { get { return _Inner; } set { _Inner = value; PropertyChanged.SafeInvoke(this,"Inner"); } }
    public event PropertyChangedEventHandler PropertyChanged;
}

class MyInner : INotifyPropertyChanged
{
    private string _SomeValue;
    public string SomeValue { get { return _SomeValue; } set { _SomeValue = value; PropertyChanged.SafeInvoke(this, "SomeValue"); } }
    public event PropertyChangedEventHandler PropertyChanged;
}
static class Program
{
    [STAThread]
    public static void Main() {
        var myObject = new MyObject();
        myObject.Name = "old name";
        // optionally start with a default
        //myObject.Inner = new MyInner();
        //myObject.Inner.SomeValue = "old inner value";

        Application.EnableVisualStyles();
        using (Form form = new Form())
        using (TextBox txtName = new TextBox())
        using (TextBox txtSomeValue = new TextBox())
        using (Button btnInit = new Button())
        {
            var outer = new BindingSource { DataSource = myObject };
            var inner = new BindingSource(outer, "Inner");
            txtName.DataBindings.Add("Text", outer, "Name");
            txtSomeValue.DataBindings.Add("Text", inner, "SomeValue");
            btnInit.Text = "all change!";
            btnInit.Click += delegate
            {
                myObject.Name = "new name";
                var newInner = new MyInner();
                newInner.SomeValue = "new inner value";
                myObject.Inner = newInner;
            };
            txtName.Dock = txtSomeValue.Dock = btnInit.Dock = DockStyle.Top;
            form.Controls.AddRange(new Control[] { btnInit, txtSomeValue, txtName });
            Application.Run(form);
        }
    }

}
person Marc Gravell    schedule 20.10.2010
comment
Эй, спасибо, Марк. Я немного добавлю код SafeInvoke, но надеялся, что сначала заработает привязка данных. К сожалению, похоже, это не имело значения. Любые другие мысли о том, что я могу делать неправильно? - person Matt Cooper; 21.10.2010
comment
@Matt - я добавлю свою тестовую установку к моему ответу - вы видите, работает ли это? Это может быть проблема с версией .NET... - person Marc Gravell; 21.10.2010
comment
Спасибо, Марк. Я обновил вопрос последними результатами вашей тестовой установки. Кажется, пока я начинаю с объекта для Inner, все работает. Если Inner равен нулю, я ничего не могу получить от элемента управления. Во всяком случае, я отметил вопрос как ответ и очень ценю помощь! - person Matt Cooper; 22.10.2010