как связать wpf datagrid с переменным числом столбцов?

<p>мое приложение wpf генерирует наборы данных, которые могут каждый раз иметь разное количество столбцов. в выводе содержится описание каждого столбца, который будет использоваться для применения форматирования. упрощенная версия вывода может выглядеть примерно так:</p><pre><code>class data { ilist<columndescription> columndescriptions { get; set; } string[][] rows { get; set; } } </code></pre><p>этот класс установлен как datacontext в wpf datagrid, но я фактически создаю столбцы программно:</p><pre><code>for (int i = 0; i < data.columndescriptions.count; i++) { datagrid.columns.add(new datagridtextcolumn { header = data.columndescriptions[i].name, binding = new binding(string.format("[{0}]", i)) }); } </code></pre><p>есть ли способ заменить этот код привязками данных в файле xaml?</p>

<p>вот обходной путь для связывания столбцов в datagrid. поскольку свойство columns является readonly, как все заметили, я создал свойство attached под названием bindablecolumns, которое обновляет столбцы в datagrid каждый раз, когда коллекция изменяется посредством события collectionchanged. </p><p>если у нас есть эта коллекция datagridcolumn's</p><pre><code>public observablecollection<datagridcolumn> columncollection { get; private set; } </code></pre><p>затем мы можем привязать bindablecolumns к columncollection следующим образом</p><pre><code><datagrid name="datagrid" local:datagridcolumnsbehavior.bindablecolumns="{binding columncollection}" autogeneratecolumns="false" ...> </code></pre><p>присоединенное свойство bindablecolumns</p><pre><code>public class datagridcolumnsbehavior { public static readonly dependencyproperty bindablecolumnsproperty = dependencyproperty.registerattached("bindablecolumns", typeof(observablecollection<datagridcolumn>), typeof(datagridcolumnsbehavior), new uipropertymetadata(null, bindablecolumnspropertychanged)); private static void bindablecolumnspropertychanged(dependencyobject source, dependencypropertychangedeventargs e) { datagrid datagrid = source as datagrid; observablecollection<datagridcolumn> columns = e.newvalue as observablecollection<datagridcolumn>; datagrid.columns.clear(); if (columns == null) { return; } foreach (datagridcolumn column in columns) { datagrid.columns.add(column); } columns.collectionchanged += (sender, e2) => { notifycollectionchangedeventargs ne = e2 as notifycollectionchangedeventargs; if (ne.action == notifycollectionchangedaction.reset) { datagrid.columns.clear(); foreach (datagridcolumn column in ne.newitems) { datagrid.columns.add(column); } } else if (ne.action == notifycollectionchangedaction.add) { foreach (datagridcolumn column in ne.newitems) { datagrid.columns.add(column); } } else if (ne.action == notifycollectionchangedaction.move) { datagrid.columns.move(ne.oldstartingindex, ne.newstartingindex); } else if (ne.action == notifycollectionchangedaction.remove) { foreach (datagridcolumn column in ne.olditems) { datagrid.columns.remove(column); } } else if (ne.action == notifycollectionchangedaction.replace) { datagrid.columns[ne.newstartingindex] = ne.newitems[0] as datagridcolumn; } }; } public static void setbindablecolumns(dependencyobject element, observablecollection<datagridcolumn> value) { element.setvalue(bindablecolumnsproperty, value); } public static observablecollection<datagridcolumn> getbindablecolumns(dependencyobject element) { return (observablecollection<datagridcolumn>)element.getvalue(bindablecolumnsproperty); } } </code></pre>

<p>я продолжил свое исследование и не нашел никакого разумного способа сделать это. свойство columns в datagrid - это не то, с чем я могу связываться, на самом деле оно доступно только для чтения.</p><p>брайан предположил, что с autogeneratecolumns можно что-то сделать, поэтому я посмотрел. он использует простое отражение .net для просмотра свойств объектов в itemssource и генерирует столбец для каждого из них. возможно, я мог бы сгенерировать тип на лету со свойством для каждого столбца, но это уже не в порядке.</p><p>так как эта проблема так легко решается в коде, я буду придерживаться простого метода расширения, который я вызываю всякий раз, когда контекст данных обновляется новыми столбцами:</p><pre><code>public static void generatecolumns(this datagrid datagrid, ienumerable<columnschema> columns) { datagrid.columns.clear(); int index = 0; foreach (var column in columns) { datagrid.columns.add(new datagridtextcolumn { header = column.name, binding = new binding(string.format("[{0}]", index++)) }); } } // e.g. mygrid.generatecolumns(schema); </code></pre>

<p>я нашел статью в блоге деборы курата с хорошим трюком, как показать переменное количество столбцов в datagrid:</p><p> <a href="http://blogs.msmvps.com/deborahk/populating-a-datagrid-with-dynamic-columns-in-a-silverlight-application-using-mvvm/">заполнение datagrid динамическими столбцами в приложении silverlight с использованием mvvm</a> </p><p>по сути, она создает <code>datagridtemplatecolumn</code> и помещает <code>itemscontrol</code> внутрь, который отображает несколько столбцов.</p>

<p>мне удалось сделать возможным динамическое добавление столбца, используя только строку кода, подобную этой:</p><pre><code>myitemscollection.addpropertydescriptor( new dynamicpropertydescriptor<user, int>("age", x => x.age)); </code></pre><p>что касается вопроса, то это не решение на основе xaml (поскольку, как уже упоминалось, нет разумного способа сделать это), а также решение, которое будет работать непосредственно с datagrid.columns. на самом деле он работает с привязанным datagrid itemssource, который реализует itypedlist и, как таковой, предоставляет пользовательские методы для извлечения propertydescriptor. в одном месте кода вы можете определить «строки данных» и «столбцы данных» для вашей сетки.</p><p>если бы вы имели:</p><pre><code>ilist<string> columnnames { get; set; } //dict.key is column name, dict.value is value dictionary<string, string> rows { get; set; } </code></pre><p>вы можете использовать, например:</p><pre><code>var descriptors= new list<propertydescriptor>(); //retrieve column name from preprepared list or retrieve from one of the items in dictionary foreach(var columnname in columnnames) descriptors.add(new dynamicpropertydescriptor<dictionary, string>(columnname, x => x[columnname])) myitemscollection = new dynamicdatagridsource(rows, descriptors) </code></pre><p>и ваша сетка с использованием привязки к myitemscollection будет заполнена соответствующими столбцами. эти столбцы могут быть изменены (добавлены новые или удалены существующие) во время выполнения динамически, и сетка автоматически обновит свою коллекцию столбцов.</p><p>упомянутый выше dynamicpropertydescriptor является просто обновлением обычного propertydescriptor и предоставляет строгое типизированное определение столбцов с некоторыми дополнительными опциями. в противном случае dynamicdatagridsource работал бы просто отлично с базовым propertydescriptor.</p>

<p>сделан вариант принятого ответа, который обрабатывает отписку.</p><pre><code>public class datagridcolumnsbehavior { public static readonly dependencyproperty bindablecolumnsproperty = dependencyproperty.registerattached("bindablecolumns", typeof(observablecollection<datagridcolumn>), typeof(datagridcolumnsbehavior), new uipropertymetadata(null, bindablecolumnspropertychanged)); /// <summary>collection to store collection change handlers - to be able to unsubscribe later.</summary> private static readonly dictionary<datagrid, notifycollectionchangedeventhandler> \_handlers; static datagridcolumnsbehavior() { \_handlers = new dictionary<datagrid, notifycollectionchangedeventhandler>(); } private static void bindablecolumnspropertychanged(dependencyobject source, dependencypropertychangedeventargs e) { datagrid datagrid = source as datagrid; observablecollection<datagridcolumn> oldcolumns = e.oldvalue as observablecollection<datagridcolumn>; if (oldcolumns != null) { // remove all columns. datagrid.columns.clear(); // unsubscribe from old collection. notifycollectionchangedeventhandler h; if (\_handlers.trygetvalue(datagrid, out h)) { oldcolumns.collectionchanged -= h; \_handlers.remove(datagrid); } } observablecollection<datagridcolumn> newcolumns = e.newvalue as observablecollection<datagridcolumn>; datagrid.columns.clear(); if (newcolumns != null) { // add columns from this source. foreach (datagridcolumn column in newcolumns) datagrid.columns.add(column); // subscribe to future changes. notifycollectionchangedeventhandler h = (\_, ne) => oncollectionchanged(ne, datagrid); \_handlers[datagrid] = h; newcolumns.collectionchanged += h; } } static void oncollectionchanged(notifycollectionchangedeventargs ne, datagrid datagrid) { switch (ne.action) { case notifycollectionchangedaction.reset: datagrid.columns.clear(); foreach (datagridcolumn column in ne.newitems) datagrid.columns.add(column); break; case notifycollectionchangedaction.add: foreach (datagridcolumn column in ne.newitems) datagrid.columns.add(column); break; case notifycollectionchangedaction.move: datagrid.columns.move(ne.oldstartingindex, ne.newstartingindex); break; case notifycollectionchangedaction.remove: foreach (datagridcolumn column in ne.olditems) datagrid.columns.remove(column); break; case notifycollectionchangedaction.replace: datagrid.columns[ne.newstartingindex] = ne.newitems[0] as datagridcolumn; break; } } public static void setbindablecolumns(dependencyobject element, observablecollection<datagridcolumn> value) { element.setvalue(bindablecolumnsproperty, value); } public static observablecollection<datagridcolumn> getbindablecolumns(dependencyobject element) { return (observablecollection<datagridcolumn>)element.getvalue(bindablecolumnsproperty); } } </code></pre>

<p>вы можете создать пользовательский контроль с определением сетки и определить «дочерние» элементы управления с различными определениями столбцов в xaml. родителю необходимо свойство зависимости для столбцов и метод для загрузки столбцов:</p><p>родитель:</p><hr><pre><code>public observablecollection<datagridcolumn> gridcolumns { get { return (observablecollection<datagridcolumn>)getvalue(columnsproperty); } set { setvalue(columnsproperty, value); } } public static readonly dependencyproperty columnsproperty = dependencyproperty.register("gridcolumns", typeof(observablecollection<datagridcolumn>), typeof(parentcontrol), new propertymetadata(new observablecollection<datagridcolumn>())); public void loadgrid() { if (gridcolumns.count > 0) mygrid.columns.clear(); foreach (datagridcolumn c in gridcolumns) { mygrid.columns.add(c); } } </code></pre><p>детский ксамл:</p><hr><pre><code><local:parentcontrol x:name="degrid"> <local:parentcontrol.gridcolumns> <toolkit:datagridtextcolumn width="auto" header="1" binding="{binding path=.}" /> <toolkit:datagridtextcolumn width="auto" header="2" binding="{binding path=.}" /> </local:parentcontrol.gridcolumns> </local:parentcontrol> </code></pre><hr><p>и, наконец, сложная задача - найти, где вызвать «loadgrid».<br> я борюсь с этим, но заставил вещи работать, вызвав после <code>initalizecomponent</code> в моем конструкторе окна (childgrid - это x: name в window.xaml):</p><pre><code>childgrid.degrid.loadgrid(); </code></pre><p> <a href="http://jvdveen.blogspot.com/2009/05/dependency-properties-and-collections.html?showcomment=1252594381091#c7610999953020859416">связанная запись в блоге</a> </p>

<p>вы можете сделать это с помощью autogeneratecolumns и datatemplate. я не уверен, если это будет работать без большой работы, вам придется поиграть с этим. честно говоря, если у вас уже есть работающее решение, я бы не стал вносить изменения, пока нет веской причины. элемент управления datagrid становится очень хорошим, но ему все еще нужно немного поработать (и мне еще многое предстоит сделать), чтобы можно было легко выполнять динамические задачи, подобные этой.</p>

<p>вот пример того, как я делаю программно:</p><pre><code>public partial class usercontrolwithcomboboxcolumndatagrid : usercontrol { private dictionary<int, string> \_dictionary; private observablecollection<myitem> \_myitems; public usercontrolwithcomboboxcolumndatagrid() { \_dictionary = new dictionary<int, string>(); \_dictionary.add(1,"a"); \_dictionary.add(2,"b"); \_myitems = new observablecollection<myitem>(); datagridmyitems.autogeneratingcolumn += datagridmyitems\_autogeneratingcolumn; datagridmyitems.itemssource = \_myitems; } private void datagridmyitems\_autogeneratingcolumn(object sender, datagridautogeneratingcolumneventargs e) { var desc = e.propertydescriptor as propertydescriptor; var att = desc.attributes[typeof(columnnameattribute)] as columnnameattribute; if (att != null) { if (att.name == "my combobox item") { var comboboxcolumn = new datagridcomboboxcolumn { displaymemberpath = "value", selectedvaluepath = "key", itemssource = \_approvaltypes, selectedvaluebinding = new binding( "bazinga"), }; e.column = comboboxcolumn; } } } } public class myitem { public string name{get;set;} [columnname("my combobox item")] public int bazinga {get;set;} } public class columnnameattribute : attribute { public string name { get; set; } public columnnameattribute(string name) { name = name; } } </code></pre>