Двусторонняя привязка «виртуального» списка строк к столбцу - PullRequest
9 голосов
/ 15 марта 2012

У меня есть список строк.

Ну, концептуально.Они хранятся где-то еще, но я хочу предоставить объект, который действует как список (и обеспечивает все необходимые события в дополнение к этому), со свойствами, которые я мог бы связать.

Я хочу установить дваспособ связывания этих данных, чтобы отобразить его как изменяемый столбец в DataGrid.У меня есть следующие проблемы с этим:

  • Я не могу сделать двустороннюю привязку, потому что привязка нуждается в пути (то есть я не могу, чтобы он выглядел как {Binding} или {Binding Path=.}в столбце должно быть {Binding Path=someField"}, чтобы его можно было изменить, если я правильно понял, что звучит разумно).
  • Я точно не знаю, как должен выглядеть объект коллекции прокси с точки зрения интерфейсов (IEnumerable + INotifyCollectionChanged достаточно?)

Существует ли какое-либо решение, которое не предусматривает создание одного прокси-объекта на каждую строку в коллекции?Не могли бы вы предложить эффективный дизайн?


Чтобы продолжить обсуждение рельсов, давайте предположим, что я хочу связать что-то вроде этого:

class Source {
    public String getRow(int n);
    public void setRow(int n, String s);
    public int getCount();
    public void addRow(int position, String s);
    public void removeRow(int position);
}

Это не совсем мой случай,но когда я знаю, как связать это, я думаю, что смогу справиться с любой ситуацией, подобной этой.

Я согласен с необходимостью предоставить объект адаптера поверх этого источника,с любыми необходимыми интерфейсами и событиями, но я не хочу иметь один объект адаптера на строку данных.

Ответы [ 7 ]

1 голос
/ 08 мая 2012

Я нахожу ваш подход немного странным.

Сетки данных обычно используются для отображения строк.Строки состоят из данных, которые принадлежат друг другу.Например, вы можете легко сопоставить строку с определенным классом.Это означает, что столбцы в вашей сетке данных представляют свойства вашего класса.

То, что вы пытаетесь сделать, - это наоборот, вы пытаетесь получить связь между значениями столбцов, а не значениями строк.

Не было бы проще иметь коллекцию вашего класса, к которой затем можно привязать столбец?

Например,

class MyClass : INotifyPropertyChanged
{
    // Remember to actually implement INotifyPropertyChanged
    string Column;
}

Если бы у вас была ObservableCollectionMyClass вы можете привязать DataGrid к этой коллекции.Всякий раз, когда свойство, которое я назвал «Столбец», изменяется, вы можете обновлять свой специальный список.

Вы можете сделать это, подключив некоторые события.Благодаря реализации INotifyPropertyChanged ваши столбцы будут обновлены, если вы обновите значение «Column» напрямую.

1 голос
/ 28 августа 2012

Хотя создание адаптера для Source относительно понятно, к сожалению, суть второй проблемы («не оборачивая каждую строку в миниобъект») - это конфликт, встроенный в .Net и WPF.

Во-первых, WPF предоставляет вам много способов регистрации обратных вызовов «с измененными данными», но не предоставляет способа регистрации обратных вызовов, которые могли бы предоставить значение.Я имею в виду, что фаза «set» является только расширяемой, а не перехватываемой, а «get» - вообще ничем.WPF просто сохранит и вернет все данные, которые он когда-то кэшировал.

Во-вторых, в .Net string является ... неизменным.

Теперь, если вы когда-нибудь предоставитеСтрока напрямую как безусловная привязка или как текстовый текст к любому элементу управления, вы ввернуты в тупик.Проблема в том, что WPF фактически передает только фактическое значение привязки, без информации «откуда она взялась».Базовому элементу управления будет просто дан экземпляр строки, и у него не будет никакого разумного способа изменить его, поскольку строка не может изменить себя.Вы даже не будете уведомлены о такой попытке, как со свойствами только для чтения.Более того - если вам когда-нибудь удастся перехватить такую ​​попытку модификации, и если вы создадите правильную новую строку, WPF никогда больше не будет запрашивать у вас новое значение.Чтобы обновить пользовательский интерфейс, вам необходимо вручную, буквально, заставить WPF повторно запросить вас, например, изменив исходную привязку, чтобы она указывала в другом месте (на новое значение), или установите текст данных (на новый экземпляр).Это возможно при некотором сканировании VisualTree, так как каждый «измененный» обратный вызов дает вам DependencyObjects (Controls!), Так что вы можете сканировать вверх / вниз и изменять их свойства. Помните, что опция - я будуобратитесь к этому через минуту.

Итак, все сводится к тому, что для получения нормального двустороннего связывания вам не нужно иметь путь, вам просто нужно иметь изменяемые базовые данные.объект.Если у вас есть неизменяемое - тогда вы должны использовать привязку к изменяемому свойству, которое содержит неизменяемое значение ..

Сказав это, вы просто должны немного обернуть строки, как если вы хотите изменить их.

Другой вопрос, как это сделать.Есть много способов сделать это.Конечно, вы можете просто обернуть их, как предложили Джо и Давио (примечание для Джо: там также понадобится INotify), или вы можете попробовать сделать некоторые трюки XAML с прикрепленными свойствами и / или поведениями и / или конвертерами, чтобы сделать это длявы.Это вполне выполнимо, см., Например, мой другой пост - я показал там, как «внедрить виртуальное свойство», которое полностью извлекало данные из другого места (один преобразователь связывания + выполнял перенос на лету,вторая привязка извлекает значения из вложенной обертки).Таким образом, вы можете создать свойство «Contents» для строки, и это свойство может просто возвращать саму строку, и она будет полностью двухсторонней, без каких-либо исключений.

Но .. это НЕ будетработать в двух направлениях.

Где-то в корне вашей цепочки связывания / поведения / конвектора будет неизменная строка .Как только ваша интеллектуальная цепочка связывания с автоматическим переносом сработает с обратным вызовом «на измененном», вы получите уведомление с парой старых / новых значений.Вы сможете переназначить значения на новые и старые строки.Если вы все реализовали идеально, WPF просто использует новое значение.Если вы где-то отключились, то вам придется искусственно возвращать новое значение в пользовательский интерфейс (см. Параметры, которые я просил вас запомнить).Итак, все в порядке.Оболочки нет, старое значение было видно, оно было изменяемым, вы получили новое значение, пользовательский интерфейс отображает новое значение.Как насчет хранения?

Где-то за это время вам дали старую / новую пару значений.Если вы проанализируете их, вы получите старые / новые строки.Но как обновить старую неизменяемую строку ?Не могу сделатьДаже если сработала автоматическая упаковка, даже если сработал пользовательский интерфейс, даже если казалось, что редактирование работало, теперь вы стоите перед реальной задачей: вы вызвали модифицированный обратный вызов, и вам нужно фактически обновить этот неизменный фрагмент строки.

Во-первых,вам нужен ваш источник.Это статично?Уф.Какая удача!Так что, конечно, это пример.В измененном обратном вызове мы получили только старую + новую строку ... как получить экземпляр Source?Опции:

  • сканировать VisualTree и искать его в текстовых данных и использовать все, что было найдено.
  • добавить еще несколько прикрепленных свойств и привязку, чтобы привязать виртуальное свойство «Источник» ккаждую строку и прочитайте это свойство из нового значения

Хорошо выполнимо, но пахнет, но других вариантов нет.

Подождите, есть еще: не только старое / новое значение иНужен экземпляр Source!Вам также нужен ИНДЕКС РЯДА.D'о!как получить это из связанных данных?Опять же, параметры:

  • сканировать VisualTree и искать его (благи) ...
  • добавить еще несколько прикрепленных свойств и привязок для привязки виртуального свойства "RowIndex" к каждому (blaaergh) ...

В данный момент, хотя я вижу, что все это кажется реализуемым и на самом деле может работать должным образом , я действительно думаю, что перенос каждой строки вмаленький

public class LocalItem // + INotifyPropertyChanged
{
    public int Index { get; }
    public Source Source { get; }

    public string Content
    {
        get { Source...}
        set { Source... }
    }
}

будет просто более удобочитаемым, элегантным и .. КОРОТКОМ для реализации.И менее подвержен ошибкам, так как больше деталей будет явным вместо привязки некоторых WPF + прикрепленной магии.

0 голосов
/ 28 августа 2012

Самый простой способ - поместить строку в класс-оболочку.

public class Wrapper
{
    public string Content{get;set;}
}

Затем вы используете строку через класс-оболочку. Это был список элементов, которые остались прежними, но содержание изменилось. Проблема в том, что когда вы делаете это без этого, тогда старая строка удаляется, а новая создается, а коллекция путается.

0 голосов
/ 22 июня 2012

Это действительно зависит от того, как вы реализуете пользовательский интерфейс.Беа Штольниц (Bea Stollnitz) провела отличную работу по виртуализации ItemsSource для WPF DataGrid на http://bea.stollnitz.com/blog/?p=344.С работой я использовал это для редактирования, а также отображения данных.

0 голосов
/ 22 апреля 2012

Ленивый раствор

Получите из ObservableCollection<string> и пусть эта коллекция будет заполнена из источника. В производном классе зарегистрируйтесь в событиях изменения коллекции и соответственно обновите источник. Свяжите столбец DataGrid с наблюдаемой коллекцией.

Это должно быть довольно просто написать, но имеет большой недостаток - дублировать все данные в коллекции.

Более эффективное решение

Создайте адаптер (как вы предложили) и внедрите IList<string> и INotifyCollectionChanged. Пусть список операций проваливается прямо к источнику. Свяжите столбец DataGrid с адаптером.

Этот подход потребовал бы утомительного шаблона, но это тонкий слой между элементом управления WPF и вашим Source.

0 голосов
/ 15 марта 2012

У меня есть этот бит кода, который я использую, чтобы связать список пользовательских объектов с DataContextMenu.Вы можете изменить его, чтобы использовать список строк и привязать его к тому, что вам нужно

class SampleCode
{
    class Team
    {
        private string _TeamName = "";
        private int _TeamProperty1 = 0;
        ObservableCollection<Territory> _Territories = new ObservableCollection<Territory>();

        public Team(string tName)
        {
            this.TeamName = tName;
        }

        public ObservableCollection<Territory> Territories
        {
            get { return _Territories; }
            set { _Territories = value; }
        }

        public string TeamName
        {
            get { return _TeamName; }
            set { _TeamName = value; }
        }

        public int TeamProperty1
        {
            get { return _TeamProperty1; }
            set { _TeamProperty1 = value; }
        }

    }

    class Territory
    {
        private string _TerritoryName = "";
        Team _AssociatedTeam = null;

        public Territory(string tName, Team team)
        {
            this.TerritoryName = tName;
            this.AssociatedTeam = team;
        }

        public Team AssociatedTeam
        {
            get { return _AssociatedTeam; }
            set { _AssociatedTeam = value; }
        }

        public string TerritoryName
        {
            get { return _TerritoryName; }
            set { _TerritoryName = value; }
        }


        public void Method1()
        {
            //Do Some Work
        }
    }

    class MyApplication
    {
        ObservableCollection<Team> _Teams = new ObservableCollection<Team>();
        ContextMenu _TeritorySwitcher = new ContextMenu();
        public MyApplication()
        {

        }

        public void AddTeam()
        {
            _Teams.Add(new Team("1"));
            _Teams.Add(new Team("2"));
            _Teams.Add(new Team("3"));
            _Teams.Add(new Team("4"));

            foreach (Team t in _Teams)
            {
                t.Territories.Add(new Territory("1", t));
                t.Territories.Add(new Territory("2", t));
                t.Territories.Add(new Territory("3", t));
            }

            SetContextMenu();
        }

        private void SetContextMenu()
        {
            HierarchicalDataTemplate _hdtTerritories = new HierarchicalDataTemplate();
            _hdtTerritories.DataType = typeof(Territory);

            HierarchicalDataTemplate _hdtTeams = new HierarchicalDataTemplate();
            _hdtTeams.DataType = typeof(Team);

            FrameworkElementFactory _TeamFactory = new FrameworkElementFactory(typeof(TreeViewItem));
            _TeamFactory.Name = "txtTeamInfo";
            _TeamFactory.SetBinding(TreeViewItem.HeaderProperty, new Binding("TeamProperty1"));

            FrameworkElementFactory _TerritoryFactory = new FrameworkElementFactory(typeof(TreeViewItem));
            _TerritoryFactory.Name = "txtTerritoryInfo";
            _TerritoryFactory.SetBinding(TreeViewItem.HeaderProperty, new Binding("TerritoryProperty1"));


            _hdtTeams.ItemsSource = new Binding("Territories");

            _hdtTeams.VisualTree = _TeamFactory;
            _hdtTerritories.VisualTree = _TerritoryFactory;

            _hdtTeams.ItemTemplate = _hdtTerritories;

            _TeritorySwitcher.ItemTemplate = _hdtTeams;
            _TeritorySwitcher.ItemsSource = this._Teams;
        }
    }
}
0 голосов
/ 15 марта 2012

Начните с ObservableCollection<string>.Затем установите для привязываемого элемента управления ItemsSource значение ObservableCollection.

...