Este artículo se centra en cómo utilizar el control TreeView. El control TreeView tiene funciones ricas y se puede utilizar en muchas situaciones. Resumen: Describe cómo agregar funcionalidad de enlace de datos al control TreeView. Es uno de una serie de ejemplos de desarrollo de controles de Microsoft Windows. Puede leer este artículo junto con el artículo de descripción general relacionado.
Introducción
Siempre que sea posible, debe comenzar con controles disponibles en el mercado porque los controles de Microsoft® Windows® Forms proporcionados incluyen tanta codificación y pruebas que sería un desperdicio abandonarlos y comenzar desde cero. En base a esto, en este ejemplo, heredaré un control existente de Windows Forms, TreeView, y luego lo personalizaré. Cuando descarga el código para el control TreeView , también obtiene ejemplos de desarrollo de controles adicionales, así como una aplicación de muestra que demuestra cómo utilizar el TreeView mejorado con otros controles vinculados a datos.
Diseño de una vista de árbol de enlace de datos
Para los desarrolladores de Windows, agregar enlace de datos al control TreeView es un problema común, pero debido a que existe una diferencia importante entre TreeView y otros controles (como ListBox o DataGrid ) (es decir, TreeView muestra datos jerárquicos), el control básico Esta característica es aún no es compatible (es decir, todavía tenemos que usarlo). Dada una tabla de datos, está claro cómo mostrar esa información en un ListBox o DataGrid , pero usar la naturaleza en capas de un TreeView para mostrar los mismos datos no es tan sencillo. Personalmente, he aplicado muchos métodos diferentes al mostrar datos usando TreeView , pero hay un método que se usa con mayor frecuencia: agrupar los datos en la tabla por ciertos campos, como se muestra en la Figura 1.
Figura 1: Visualización de datos en TreeView
En este ejemplo, crearé un control TreeView en el que puedo pasar un conjunto de datos plano (que se muestra en la Figura 2) y producir fácilmente los resultados que se muestran en la Figura 1.
Figura 2: Conjunto de resultados plano que contiene toda la información necesaria para crear el árbol que se muestra en la Figura 1
Antes de comenzar a codificar, se me ocurrió un diseño para el nuevo control que manejaría este conjunto de datos en particular y, con suerte, funcionaría para muchas otras situaciones similares. Agregue una colección de grupos lo suficientemente grande como para crear una jerarquía utilizando la mayoría de los datos planos, en la que especifica un campo de agrupación, un campo de visualización y un campo de valor para cada nivel de jerarquía (cualquiera o todos los campos deben ser iguales). Para transformar los datos que se muestran en la Figura 2 en el TreeView que se muestra en la Figura 1, mi nuevo control requiere que usted defina dos niveles de agrupación, Editor y Título, y defina pub_id como el campo de agrupación del grupo de Editores y title_id como el campo de agrupación. del campo Grupo de título. Además de los campos de agrupación, también debe especificar los campos de visualización y valor para cada grupo para determinar el texto que se muestra en el nodo del grupo correspondiente y el valor que identifica de forma única al grupo específico. Cuando encuentre este tipo de datos, utilice pub_name/pub_id y title/title_id como campos de visualización/valor para estos dos grupos. La información del autor se convierte en los nodos hoja del árbol (los nodos al final de la jerarquía de agrupación), y también debe especificar los campos ID ( au_id ) y visualización ( au_lname ) para estos nodos.
Al crear un control personalizado, determinar cómo el programador utilizará el control antes de comenzar a codificar ayudará a que el control sea más eficiente. En este caso, me gustaría que el programador (dados los datos mostrados anteriormente y el resultado deseado) pudiera realizar la agrupación con unas pocas líneas de código como este:
Con DbTreeControl .ValueMember = au_id .DisplayMember = au_lname .DataSource = myDataTable.DefaultView .AddGroup(Editor, pub_id, pub_name, pub_id) .AddGroup(Título, título_id, título, título_id) Terminar con |
Nota: Esta no es la última línea de código que escribí, pero es similar. Mientras desarrollaba el control, me di cuenta de que necesitaba asociar el índice de imágenes en ImageList asociado con TreeView con cada nivel de agrupación, por lo que tuve que agregar un parámetro adicional al método AddGroup .
Para construir realmente el árbol, revisaré los datos y buscaré cambios en los campos (especificados como los valores de agrupación para cada grupo) mientras creo nuevos nodos de agrupación si es necesario y un nodo hoja para cada elemento de datos. Debido a los nodos de agrupación, el número total de nodos será mayor que el número de elementos en la fuente de datos, pero habrá exactamente un nodo hoja para cada elemento en los datos subyacentes.
Figura 3: Nodos de grupo y nodos de hoja
La distinción entre nodos hoja y nodos de grupo (que se muestra en la Figura 3) será importante durante el resto de este artículo. Decidí tratar estos dos tipos de nodos de manera diferente, crear nodos personalizados para cada tipo de nodo y generar diferentes eventos según el tipo de nodo seleccionado.
Implementar enlace de datos
El primer paso para escribir código para este control es crear el proyecto y la clase inicial correspondiente. En este ejemplo, primero creo una nueva biblioteca de controles de Windows, luego elimino la clase UserControl predeterminada y la reemplazo con una nueva clase que hereda del control TreeView :
Clase pública dbTreeControl
Hereda System.Windows.Forms.TreeView
A partir de este momento, diseñaré un control que se pueda colocar en un formulario y que tenga el aspecto y la funcionalidad de un TreeView normal. El siguiente paso es comenzar a agregar el código necesario para manejar la nueva funcionalidad que se agrega a TreeView , es decir, enlace de datos y agrupación de datos.
Agregar propiedad de origen de datos
Toda la funcionalidad de mi nuevo control es importante, pero dos cuestiones clave en la creación de controles complejos vinculados a datos son el manejo de las propiedades de DataSource y la recuperación de elementos individuales de cada objeto de la fuente de datos.
Crear rutina de atributos
Primero, cualquier control utilizado para implementar un enlace de datos complejo debe implementar una rutina de propiedad DataSource y mantener las variables miembro apropiadas:
m_DataSource privado como objeto _ Propiedad pública DataSource() como objeto Conseguir Devolver m_DataSource Fin de obtención Establecer (valor ByVal como objeto) Si el valor no es nada entonces centímetros = nada AgrupaciónCambiada() Demás Si no (el valor TypeOf es IList o _ El valor TypeOf es IListSource) Entonces ' no es una fuente de datos válida para este propósito Lanzar nuevo System.Exception (fuente de datos no válida) Demás Si el valor TypeOf es IListSource, entonces Atenuar myListSource como IListSource myListSource = CType(Valor, IListSource) Si myListSource.ContainsListCollection = True Entonces Lanzar nuevo System.Exception (fuente de datos no válida) Demás 'Sí, sí. es una fuente de datos válida m_DataSource = Valor cm = CType(Me.BindingContext(Valor), _ Administrador de divisas) AgrupaciónCambiada() Terminar si Demás m_DataSource = Valor cm = CType(Me.BindingContext(Valor), _ Administrador de divisas) AgrupaciónCambiada() Terminar si Terminar si Terminar si Conjunto final Propiedad final |
Generalmente se admiten objetos que se pueden utilizar como fuentes de datos para enlaces de datos complejos. Esta interfaz expone los datos como una colección de objetos y proporciona varias propiedades útiles, como Count . Mi nuevo control TreeView requiere un objeto respaldado por IList en su enlace, pero usar otra interfaz funciona bien porque proporciona una manera conveniente de obtener un objeto IList ( GetList ). Al configurar la propiedad DataSource , primero determino si se proporciona un objeto válido, es decir, un objeto que admita IList o IListSource . Lo que realmente quiero es un IList , por lo que si el objeto solo admite IListSource (por ejemplo, DataTable ), usaré el método GetList() de esa interfaz para obtener el objeto correcto.
Algunos objetos que implementan IListSource (como DataSet ) en realidad contienen varias listas representadas por la propiedad ContieneListCollection . Si esta propiedad es True , GetList devolverá un objeto IList que representa una lista (que contiene varias listas). En mi ejemplo, decidí admitir conexiones directas a objetos IList u objetos IListSource que solo contienen un objeto IList e ignorar los objetos que requieren trabajo adicional para especificar una fuente de datos, como un DataSet .
Nota: Si desea admitir dichos objetos ( DataSet o similar), puede agregar una propiedad adicional (como DataMember ) para especificar una sublista específica para el enlace.
Si la fuente de datos proporcionada es válida, el resultado final es una instancia creada ( cm = Me.BindingContext(Value) ). Debido a que esta instancia se utilizará para acceder a la fuente de datos subyacente, las propiedades del objeto y la información de ubicación, se almacena en variables locales.
Agregar propiedades de miembro de visualización y valor
Tener un DataSource es el primer paso para implementar un enlace de datos complejo, pero el control necesita saber qué campos o propiedades específicos de los datos se utilizarán como miembros de visualización y valor. El miembro Display se utilizará como título del nodo del árbol, mientras que se puede acceder al miembro Value a través de la propiedad Value del nodo. Todas estas propiedades son cadenas que representan nombres de campos o propiedades y se pueden agregar fácilmente al control:
Privado m_ValueMember como cadena Privado m_DisplayMember como cadena _ Propiedad pública ValueMember() como cadena Conseguir Devolver m_ValueMember Fin de obtención Establecer (valor ByVal como cadena) m_ValueMember = Valor Conjunto final Propiedad final _ Propiedad pública DisplayMember() como cadena Conseguir Devolver m_DisplayMember Fin de obtención Establecer (valor ByVal como cadena) m_DisplayMember = Valor Conjunto final Propiedad final |
En este TreeView , estas propiedades solo representarán los miembros Display y Value de los nodos hoja, y la información correspondiente para cada nivel de agrupación se especificará en el método AddGroup .
Usando el objeto CurrencyManager
En la propiedad DataSource analizada anteriormente, se crea una instancia de la clase CurrencyManager y se almacena en una variable de nivel de clase. La clase CurrencyManager a la que se accede a través de este objeto es una parte clave de la implementación del enlace de datos porque tiene propiedades, métodos y eventos que habilitan las siguientes funciones:
Recuperar valor de atributo/campo
El objeto CurrencyManager le permite recuperar valores de propiedad o campo, como el valor de un campo DisplayMember o ValueMember , de un elemento individual en una fuente de datos a través de su método GetItemProperties . Luego use un objeto PropertyDescriptor para obtener el valor de un campo o propiedad específica en un elemento de lista específico. El siguiente fragmento de código muestra cómo crear estos objetos PropertyDescriptor y cómo usar la función GetValue para obtener el valor de propiedad de un elemento en la fuente de datos subyacente. Tenga en cuenta la propiedad List del objeto CurrencyManager : proporciona acceso a la instancia IList a la que está vinculado el control:
Atenuar myNewLeafNode como TreeLeafNode Dim currObject como objeto currObject = cm.List(currentListIndex) Si Me.DisplayMember <> Y también Me.ValueMember <> Entonces '¿Agregar nodo hoja? Dim pdValue como System.ComponentModel.PropertyDescriptor Dim pdDisplay como System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Yo.DisplayMember) miNuevoNodoHoja = _ Nuevo TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ objeto curr, _ pdValue.GetValue(objetocurr), _ índice de lista actual) |
GetValue ignora el tipo de datos subyacente de la propiedad cuando devuelve un objeto, por lo que el valor de retorno debe convertirse antes de usarlo.
Mantenga sincronizados los controles vinculados a datos
El CurrencyManager tiene una característica importante más: además de proporcionar acceso a fuentes de datos vinculadas y propiedades de elementos, permite utilizar el mismo DataSource para coordinar la vinculación de datos entre este control y cualquier otro control. Este soporte se puede utilizar para garantizar que varios controles vinculados a la misma fuente de datos al mismo tiempo permanezcan en el mismo elemento de la fuente de datos. Para mi control, quiero asegurarme de que cuando se selecciona un elemento en el árbol, todos los demás controles vinculados a la misma fuente de datos apunten al mismo elemento (el mismo registro, fila o incluso matriz, si lo desea). pensar en términos de una base de datos). Para hacer esto, anulo el método OnAfterSelect en el TreeView base. En ese método (que se llama después de seleccionar el nodo del árbol), configuro la propiedad Posición del objeto CurrencyManager en el índice del elemento seleccionado actualmente. La aplicación de ejemplo proporcionada con el control TreeView ilustra cómo los controles de sincronización facilitan la creación de interfaces de usuario vinculadas a datos. Para que sea más fácil determinar la posición en la lista del elemento seleccionado actualmente, utilicé una clase TreeNode personalizada ( TreeLeafNode o TreeGroupNode ) y almacené el índice de la lista de cada nodo en la propiedad Position que creé:
Anulaciones protegidas Sub OnAfterSelect _ (ByVal y As System.Windows.Forms.TreeViewEventArgs) Dim tln como TreeLeafNode Si TypeOf e.Node es TreeGroupNode entonces tln = FindFirstLeafNode(e.Nodo) Atenuar groupArgs como nuevo groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(groupArgs) De lo contrario, si TypeOf e.Node es TreeLeafNode, entonces Dim leafArgs como nueva hojaTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(leafArgs) tln = CType(e.Nodo, TreeLeafNode) Terminar si Si no es nada entonces Si cm.Position <> tln.Position Entonces cm.Posición = tln.Posición Terminar si Terminar si MiBase.OnAfterSelect(e) Subtítulo final |
En el fragmento de código anterior, es posible que hayas notado una función llamada FindFirstLeafNode , que quiero presentar brevemente aquí. En mi TreeView , solo los nodos hoja (los nodos finales en la jerarquía) corresponden a elementos en DataSource , todos los demás nodos solo se usan para crear la estructura de agrupación. Si quiero crear un control vinculado a datos de buen rendimiento, siempre necesito seleccionar un elemento correspondiente a DataSource , de modo que cada vez que selecciono un nodo de grupo, encuentro el primer nodo hoja debajo del grupo, como si este nodo fuera el selección actual. Puedes ver el ejemplo en acción, pero por ahora puedes usarlo con total libertad.
Función privada FindFirstLeafNode (ByVal currNode como TreeNode) _ Como árbolhojanodo Si TypeOf currNode es TreeLeafNode entonces Devolver CType(currNode, TreeLeafNode) Demás Si currNode.Nodes.Count > 0 Entonces Devolver FindFirstLeafNode(currNode.Nodes(0)) Demás No devolver nada Terminar si Terminar si Función final |
Establecer la propiedad Posición del objeto CurrencyManager sincroniza otros controles con la selección actual, pero cuando la posición de otros controles cambia, CurrencyManager también genera eventos para que la selección cambie en consecuencia. Para ser un buen componente vinculado a datos, la selección debe moverse a medida que cambia la ubicación de la fuente de datos y la visualización debe actualizarse cuando se modifican los datos de un elemento. Hay tres eventos generados por CurrencyManager : CurrentChanged , ItemChanged y PositionChanged . El último evento es bastante simple; uno de los propósitos de CurrencyManager es mantener un indicador de posición actual para la fuente de datos de modo que varios controles vinculados puedan mostrar el mismo registro o elemento de lista, y este evento se genera cada vez que cambia la posición. Los otros dos acontecimientos a veces se superponen y la distinción es menos clara. A continuación se describe cómo utilizar estos eventos en controles personalizados: PositionChanged es un evento relativamente simple y no se describirá aquí cuando desee ajustar el elemento seleccionado actualmente en un control complejo vinculado a datos (como Tree), utilice The; evento. El evento ItemChanged se genera cada vez que se modifica un elemento en la fuente de datos, mientras que CurrentChanged se genera solo cuando se modifica el elemento actual.
En mi TreeView , descubrí que cada vez que seleccionaba un elemento nuevo, se generaban los tres eventos, por lo que decidí manejar el evento PositionChanged cambiando el elemento seleccionado actualmente y no hacer nada con los otros dos. Se recomienda convertir la fuente de datos a IBindingList (si la fuente de datos admite IBindingList ) y usar el evento ListChanged en su lugar, pero no implementé esta funcionalidad.
Sub privado cm_PositionChanged (ByVal remitente como objeto, _ ByVal e As System.EventArgs) Maneja cm.PositionChanged Dim tln como TreeLeafNode Si TypeOf Me.SelectedNode es TreeLeafNode entonces tln = CType(Me.SelectedNode, TreeLeafNode) Demás tln = FindFirstLeafNode(Me.SelectedNode) Terminar si Si tln.Position <> cm.Position Entonces Me.SelectedNode = FindNodeByPosition(cm.Position) Terminar si Subtítulo final Función de sobrecarga privada FindNodeByPosition (índice ByVal como entero) _ Como nodo de árbol Devolver FindNodeByPosition(índice, Me.Nodes) Función final Función de sobrecarga privada FindNodeByPosition (índice ByVal como entero, _ ByVal NodesToSearch como TreeNodeCollection) como TreeNode Atenuar i como entero = 0 Dim currNode como TreeNode Dim tln como TreeLeafNode Hacer mientras i < NodesToSearch.Count currNode = NodosParaBuscar(i) yo += 1 Si TypeOf currNode es TreeLeafNode entonces tln = CType(currNode, TreeLeafNode) Si tln.Position = index Entonces Devolver nodo actual Terminar si Demás currNode = FindNodeByPosition(índice, currNode.Nodes) Si no currNode no es nada, entonces Devolver nodo actual Terminar si Terminar si Bucle No devolver nada Función final |
Convertir fuente de datos en árbol
Después de escribir el código de enlace de datos, puedo continuar y agregar el código para administrar los niveles de agrupación, construir el árbol en consecuencia y luego agregar algunos eventos, métodos y propiedades personalizados.
grupo de gestión
Para configurar una colección de grupos, el programador debe crear las funciones AddGroup , RemoveGroup y ClearGroups . Cada vez que se modifica la colección de grupo, se debe volver a dibujar el árbol (para reflejar la nueva configuración), por lo que creé un procedimiento genérico, GroupingChanged , que puede ser llamado mediante varios códigos en el control cuando las circunstancias cambian y es necesario forzar el árbol. ser reconstruido:
Grupos de árboles privados como nueva ArrayList () Subgrupo público RemoveGroup (grupo ByVal como grupo) Si no es treeGroups.Contains (grupo) Entonces treeGroups.Remove(grupo) AgrupaciónCambiada() Terminar si Subtítulo final Sobrecargas públicas Sub AddGroup (grupo ByVal como grupo) Intentar treeGroups.Add(grupo) AgrupaciónCambiada() Atrapar Finalizar intento Subtítulo final Sobrecargas públicas Sub AddGroup (nombre ByVal como cadena, _ Grupo ByValPor As String, _ ByVal displayMember como cadena, _ ByVal valorMiembro como cadena, _ ByVal imageIndex como entero, _ ByVal selectedImageIndex como entero) Atenuar myNewGroup como nuevo grupo (nombre, grupo por, _ miembro de visualización, miembro de valor, _ índice de imagen, índice de imagen seleccionado) Yo.AddGroup(miNuevoGrupo) Subtítulo final Función pública GetGroups() como grupo() Devuelve CType(treeGroups.ToArray(GetType(Grupo)), Grupo()) Función final |
árbol de expansión
La reconstrucción real del árbol se realiza mediante un par de procesos: BuildTree y AddNodes . Dado que el código para estos dos procesos es demasiado largo, este artículo no los enumera todos, pero intenta resumir su comportamiento (por supuesto, puede descargar el código completo si lo desea). Como se mencionó anteriormente, los programadores pueden interactuar con este control configurando una serie de grupos y luego usando estos grupos en BuildTree para determinar cómo configurar los nodos del árbol. BuildTree borra la colección de nodos actual y luego recorre en iteración toda la fuente de datos para procesar la agrupación de primer nivel (el editor mencionó en el ejemplo y el diagrama anteriormente en este artículo), agregando un nodo para cada valor de agrupación diferente (usando los datos del ejemplo, para cada Agregue un nodo con un valor pub_id ) y luego llame a AddNodes para completar todos los nodos bajo la agrupación de primer nivel. AddNodes se llama a sí mismo de forma recursiva para manejar tantos niveles como sea necesario, agregando nodos de grupo y nodos de hoja según sea necesario. Utilice dos clases personalizadas basadas en TreeNode para distinguir los nodos de grupo y los nodos de hoja, y proporcione los atributos correspondientes para los dos tipos de nodos.
Eventos personalizados de TreeView
Cada vez que se selecciona un nodo, TreeView genera dos eventos: BeforeSelect y AfterSelect . Pero bajo mi control, quería hacer que los eventos de los nodos de grupo y los nodos de hoja fueran diferentes, así que agregué mis propios eventos BeforeGroupSelect/AfterGroupSelect y BeforeLeafSelect/AfterLeafSelect . Además de los eventos básicos, también activé una clase de parámetro de evento personalizado:
Evento público BeforeGroupSelect _ (ByVal remitente como objeto, ByVal y como grupoTreeViewCancelEventArgs) Evento público AfterGroupSelect _ (ByVal remitente como objeto, ByVal y como grupoTreeViewEventArgs) Evento públicoBeforeLeafSelect _ (ByVal remitente como objeto, ByVal y como leafTreeViewCancelEventArgs) Evento público AfterLeafSelect _ (ByVal remitente como objeto, ByVal y como leafTreeViewEventArgs) Anulaciones protegidas Sub OnBeforeSelect _ (ByVal y As System.Windows.Forms.TreeViewCancelEventArgs) Si TypeOf e.Node es TreeGroupNode entonces Dim groupArgs como nuevo grupoTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs) De lo contrario, si TypeOf e.Node es TreeLeafNode, entonces Dim leafArgs como nueva hojaTreeViewCancelEventArgs(e) RaiseEvent BeforeLeafSelect(CObj(Me), leafArgs) Terminar si MiBase.OnBeforeSelect(e) Subtítulo final Anulaciones protegidas Sub OnAfterSelect _ (ByVal y As System.Windows.Forms.TreeViewEventArgs) Dim tln como TreeLeafNode Si TypeOf e.Node es TreeGroupNode entonces tln = FindFirstLeafNode(e.Nodo) Atenuar groupArgs como nuevo groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(CObj(Me), groupArgs) De lo contrario, si TypeOf e.Node es TreeLeafNode, entonces Dim leafArgs como nueva hojaTreeViewEventArgs(e) RaiseEvent AfterLeafSelect(CObj(Me), leafArgs) tln = CType(e.Nodo, TreeLeafNode) Terminar si Si no es nada entonces Si cm.Position <> tln.Position Entonces cm.Posición = tln.Posición Terminar si Terminar si MiBase.OnAfterSelect(e) Subtítulo final |
Las clases de nodos personalizados ( TreeLeafNode y TreeGroupNode ) y las clases de parámetros de eventos personalizados se incluyen en el código descargable.
Aplicación de muestra
Para comprender completamente todo el código de este control de muestra, debe comprender cómo funciona en su aplicación. La aplicación de muestra incluida utiliza la base de datos de Access pubs.mdb e ilustra cómo se puede utilizar el control Árbol con otros controles vinculados a datos para crear aplicaciones de Windows. Las características clave de particular interés en este caso incluyen la sincronización del árbol con otros controles vinculados y la selección automática de nodos del árbol al realizar una búsqueda en la fuente de datos.
Nota: Esta aplicación de muestra (llamada TheSample) se incluye en la descarga de este artículo.
Figura 4: Aplicación de demostración para TreeView vinculado a datos
resumen
El control Árbol vinculado a datos descrito en este artículo no es adecuado para todos los proyectos que requieren un control Árbol para mostrar información de la base de datos, pero introduce un método para personalizar el control para fines personales. Recuerde que cualquier control complejo vinculado a datos que desee crear tendrá gran parte del mismo código que el control Árbol, y puede simplificar el desarrollo futuro de controles modificando el código existente.