Click here to Skip to main content
Licence CPOL
First Posted 21 Sep 2011
Views 9,813
Downloads 1,126
Bookmarked 31 times

Controls Library: Extended ListView with Column Mapping

By | 4 Oct 2011 | Article
Docking windows container, extended listview, extended property editor

aistest.jpg

Localize editor

aistest.jpg

Columns map

aistest.jpg

Edit properties/ sub properties

Introduction

I have always wanted to have on hand a really easy and simple to use set of GUI classes for WinForms. It must be written in 100% C#, compatible with Mono, and do all the painting! This would enable me to work on tasks and not on how to present them in a form. This implies the ability to display and\or edit any object or collection with minimum preset and at the same time have all kinds and flexible configurations for developers and for end-users.

The library contains a few bits and pieces of controls. Let us consider the generic list, for viewing and editing of collections. The first one needed is a set of tools (ais.tool) to work with collections and objects of any type in runtime, serialization, and related tasks.

Tools ais.tool.dll

Extended List

ais.tool.ListExtend

A special list that can improve performance and do several related tasks (filtering, changing, adding, store sorting information). 

Type Service ais.tool.TypeService

You can slice work with System.Type, System.Reflection and get custom attributes from System.ComponentModel.

//several methods from TypeService class
//check is specified type implement IDictionary interface
public static bool IsDictionary (Type type)
{
    return (type.GetInterface ("IDictionary") != null 
      && type != typeof(byte[]) && type != typeof(Image));
}
...
//check is specified property have [BrowAable(false/true)] attribute
public static bool GetBrowsable (PropertyInfo property)
{
    object[] dscArray = 
       property.GetCustomAttributes (typeof(BrowsableAttribute), false);
    if (dscArray.Length == 0)
        return true;
    return ((BrowsableAttribute)dscArray [0]).Browsable;
}
...

Reflection Access

ais.tool.ReflectionAccessor

Created for performance reasons (large collection, get value of properties direct from paint event). Access through Reflection.PropertyInfo.Get/SetValue is much slower than direct access, but direct access can't give us flexibility. To eliminate this restriction, create a special wrapper: ReflectionAccessor which creates a dynamic method and delegates to this method, which significantly reduces the delay to appeal to the properties of objects. And create a static cache of created accessors. Works with MethodInfo, PropertyInfo, and ConstructorInfo (in ais.test application, you can compare delays).

//return cached ctor accessor for specified type with specified parameters param
public static ReflectionAccessor InitCreateAccessor (Type type, Type[] param)
{
    if (cacheAccessors.Contains (type))
        return (ReflectionAccessor)cacheAccessors [type];
    ConstructorInfo ci = type.GetConstructor (param);
    if (ci == null)
        return null;
    ReflectionAccessor pa = new ReflectionAccessor (ci);
    cacheAccessors.Add (type, pa);
    return pa;
}

Localization System

ais.tool.Localize

A simple localization (that can be saved in a file and can be easily edited) user API is realized with only one static function Localize.Get("category", "name"). The code below localizes string CString and class LocalizeItem for store Category (an example can be the name of a Form) and original Name. Fast data retrieval is implemented by the LocalizeItemList class by generating indexes for category and name.

//create index for value on LocalizeItemList class
public void AddIndex (string category, string name, LocalizeItem value)
{
    Dictionary<string, LocalizeItem > categoryIndex = null;
 
    if (index.ContainsKey (category))
        categoryIndex = index [category];
    else {
        categoryIndex = new Dictionary<string, LocalizeItem> ();
        index.Add (category, categoryIndex);
    }
    if (categoryIndex.ContainsKey (name))
        categoryIndex [name] = value;
    else {
        categoryIndex.Add (name, value);
    }    
}

Tools

ais.tool.Tools

Formatting and parsing byte[], images, date, etc., is done in ais.tool.Serialization..

//parse System.Drawing.Image from base64 from xml 
public static Image ImageFromBase64 (string text)
{
    try {
        if (text == "")
            return null;
        byte[] memBytes = Convert.FromBase64String (text);
        return ImageFromByte (memBytes);
    } catch {
        return null;
    }
}
//parse System.Drawing.Image from byte[]    
public static Image ImageFromByte (byte[] bytes)
{
    Image img = null;
    using (MemoryStream stream = new MemoryStream (bytes)) {
        img = Image.FromStream (stream);
    }
    return img;
}

Serialization

ais.tool.Serialization

Target to fast, and compact XML serialization of any objects, used ais.tool.Tools and ais.tool.TypeService. For support class loader: use special type naming: Type.Name with Assembly.Name. Have special instructions for collection and dictionaries.

//format specified System.object value to specified XmlNode owner
public static void FormatXml (XmlNode owner, object value, string name, bool valueType)
{
    if (value == null)
        return;
    Type type = value.GetType ();
    //check type if is valuetype then serialize as XmlAttribute
    if (TypeService.IsValueType (type) && !valueType) {
        ((XmlElement)owner).SetAttribute (name, Tool.TextFormat (value, "binary"));
    } else if (CheckIFile && value is IFSerialize && !(owner is XmlDocument)) {
        //if implement IFileSerialize then serialize on separate file
        XmlElement element = (XmlElement)owner.AppendChild 
		(owner.OwnerDocument.CreateElement (name));
        IFSerialize ifl = value as IFSerialize;
        ifl.Save (Environment.CurrentDirectory);
        ((XmlElement)element).SetAttribute ("FileName", ifl.FileName);
    } else {
        //else create XmlElement and recursive write fields of object to node 
        XmlElement element = (XmlElement)owner.AppendChild (owner is XmlDocument ? 
          ((XmlDocument)owner).CreateElement (name) : 
		owner.OwnerDocument.CreateElement (name));
        if (valueType) {
            string typeName = type.FullName + ", " + type.Assembly.GetName ().Name;
            ((XmlElement)element).SetAttribute ("VT", typeName);
            if (TypeService.IsValueType (type))
                ((XmlElement)element).SetAttribute 
			(name, Tool.TextFormat (value, "binary"));
        }
        if (TypeService.IsDictionary (type)) {
            //special for dictionary
            IDictionary collection = value as IDictionary;
            foreach (DictionaryEntry item in collection) {
                FormatXml (element, item, "i", false);
            }
                 
        } else if (TypeService.IsList (type)) {
            ///special for list
            IList collection = value as IList;
            foreach (object item in collection)
                FormatXml (element, item, "i", true);
        } else{
            //write fields
            FieldInfo[] fields = TypeService.GetFields (type, true);
            foreach (FieldInfo field in fields) {
                object v = field.GetValue (value);
                if (TypeService.IsNonSerialized (field))
                    continue;
                if (!TypeService.CheckDefault (field, v)) {
                    FormatXml (element, v, field.Name, field.FieldType == 
                      typeof(object) || field.FieldType.IsInterface ? true : false);
                }
            } 
        }
    }
}

Controls

ais.ctrl.dll

Tool Form

ais.ctrl.ToolForm

To implement custom editing in list, ComboBox like controls. Support: forms contain several open state and show with align to calling control. Extended by ToolTipForm class to implement ToolTip like controls.  

Docked Windows

ais.ctrl.DockContainer

Primitive docking windows system, support:

  • Dock any object derived from System.Windows.Form.Control
  • Left, right, top, bottom and center content align

Simple realization based on System.Windows.Form.ToolStrip and System.Windows.Form.SplitContainer. Created by IDockContainer and three classes that implement this interface:

public interface IDockContainer
{
    IDockContainer DockParent{ get; }
    
    List<IDockContainer> Docks{ get; }
    
    bool ContainsControl (Control c);
    
    bool Remove (Control control);
    
    void Add (Control control);
    
    List<Control> GetControls ();
}
...
//For direct use
public class DockContainer : UserControl, IDockContainer
{
    DockPanel panel;
    private List<IDockContainer> docks = new List<IDockContainer> (1);
    //get IDockContainer from standard Parent property of specified control 
    public static IDockContainer GetDockParent (Control control)
    {
        Control c = control.Parent;
        while (c!=null&&!(c is IDockContainer))
            c = c.Parent;
        return c as IDockContainer;
    }
    //get IDockContainer with specified name from
    //standard Parent property of specified control
    public static IDockContainer GetDockParent (Control control, string name)
    {
        Control c = control.Parent;
        while (c!=null&&c.Name!=name)
            c = GetDockParent (c) as Control;
        return c as IDockContainer;
    }
         
    public DockContainer ()
    {
        InitializeComponent ();
    }
    //add specified control to container 
    public void Add (Control c, DockType type)
    {
        if (ContainsControl (c)) {
            DockTool page = PickTool (c);
            if (page != null)
                ((DockPanel)page.Owner.Parent).SelectPage (page);
        } else {
            DockSplit split = null;
            if (type == DockType.Content) {
                panel.Add (c);
            } else if (type == DockType.Left) {
                split = GetDockParent (panel, "Left") as  DockSplit;
                if (split == null) { 
                    split = CreateSplit ("Left", 
                      Orientation.Vertical, FixedPanel.Panel1);
                    Replace (panel, split);
                    split.SplitterDistance = 220; 
                    split.DockPanel2 = panel;
                }
                split.DockPanel1.Add (c);
            } else if (type == DockType.Right) {
                split = GetDockParent (panel, "Right") as  DockSplit;
                if (split == null) { 
                    split = CreateSplit ("Right", 
                      Orientation.Vertical, FixedPanel.Panel2);
                    Replace (panel, split);
                    split.SplitterDistance = split.Width - 220; 
                    split.DockPanel1 = panel;
                }
                split.DockPanel2.Add (c);
            } else if (type == DockType.Top) {
                split = GetDockParent (panel, "Top") as  DockSplit;
                if (split == null) { 
                    split = CreateSplit ("Top", 
                      Orientation.Horizontal, FixedPanel.Panel1);
                    Replace (panel, split);
                    split.DockPanel2 = panel;
                }
                split.DockPanel1.Add (c);
            } else if (type == DockType.Bottom) {
                split = GetDockParent (panel, "Bottom") as  DockSplit;
                if (split == null) { 
                    split = CreateSplit ("Bottom", 
                      Orientation.Horizontal, FixedPanel.Panel2);
                    Replace (panel, (DockSplit)split);
                    ((DockSplit)split).DockPanel1 = panel;
                }
                split.DockPanel2.Add (c);
            }
        }
    }

    ...
	//Used for tabbed navigation between controls 
	public class DockPanel : UserControl, IEnumerable, IDockContainer
    {
        private DockToolStrip tools;
        private Panel panel;
        private List<IDockContainer> docks = 
                  new List<IDockContainer> (0);

        public DockPanel ():base()
        {
            Dock = DockStyle.Fill;
            InitializeComponent ();
            tools.Renderer = new DockRenderer ();
        }

        public void Add (DockTool page)
        {
            tools.Items.Add (page);
        }
		...
		//simple SplitContainer with IDockContainer implementation
		public class DockSplit : SplitContainer, IDockContainer
	    {
	        private List<IDockContainer> docks = 
	                    new List<IDockContainer> (2);
	
	        public DockSplit ():base()
	        {
	            Dock = DockStyle.Fill;
	        }

Control Service

ais.ctrl.Service

Primary task of this class is paint all of list (columns, glyph, text, image) and related task.

public static void PaintGliph (Graphics graphics, Rectangle r, float angle)
{
	if (gliphTexture == null) {

		GraphicsPath gliphpath = new GraphicsPath ();
		gliphpath.AddLine (2, 8, 2, 4);
		gliphpath.AddLine (0, 4, 4, 0);
		gliphpath.AddLine (8, 4, 6, 4);
		gliphpath.AddLine (6, 8, 2, 8);
		gliphpath.CloseFigure ();

		Bitmap bmp = new Bitmap (8, 8);
		Graphics g = Graphics.FromImage (bmp);

		Brush gb = new SolidBrush (Color.Black);
				
		g.DrawPath (Pens.Black, gliphpath);
		gliphTexture = new TextureBrush (bmp, WrapMode.Clamp);

		g.FillPath (gb, gliphpath);
		gliphFillTexture = new TextureBrush (bmp, WrapMode.Clamp);
				
		gb.Dispose ();
		gliphpath.Dispose ();
		g.Dispose ();
	}
			
	gliphFillTexture.ResetTransform ();
	Matrix m = new Matrix ();
	if (angle != 0) {
		m.RotateAt (angle, new PointF (4, 4));
	}
	Matrix m2 = new Matrix ();
	m2.Translate (r.X, r.Y);
	gliphFillTexture.MultiplyTransform (m, MatrixOrder.Append);
	gliphFillTexture.MultiplyTransform (m2, MatrixOrder.Append);
	graphics.FillRectangle (gliphFillTexture, r);
}

List Viewer

ais.ctrl.PList

View list of any collection (alternative for ListView in detailed mode, DataGridView in virtual mode):

  • Inline properties (object.Property1.Property2...)
  • Universal, minimal configuration
  • Extended column manipulation (drag&drop and sizing columns by mouse)
  • Large collection support (grouping can slowdown)
  • Sorting. Grouping
  • Serializable (PListInfo class easy to save and have all basic information about list)
  • Editing

Column mapping realized by two classes that implement IPColumn:  PListColumn and PListColumnMap.

public interface IPColumn
{
    int Height { get; set; }

    int Width { get; set; }

    int RowIndex { get; set; }

    int ColIndex { get; set; }

    bool Visible { get; }

    string Property { get; }

    PListColumnMap Map { get; set; }
}
...
//moving specified column(moved) to specified side(anch) of other column(destination)
//inside the PListColumnMap
public void Move (IPColumn moved, IPColumn destination, AnchorStyles anch, bool builGroup)
{
    //check collision 
    if (moved.Map.Contains (destination) && 
           moved.Map != destination.Map && moved.Map.Map != null)
        return;
    if (moved.Map == destination.Map && destination.Map.Map != null)
        builGroup = false;

    PListColumnMap mowner = moved.Map;
    //remove from old map
    mowner._Remove (moved);
    //check map is empty
    if (mowner.Count == 0) {
        mowner.Map._Remove (mowner);
    }
    //check map is not empty but have only one item
    if (mowner != destination.Map && 
            mowner.Count == 1 && mowner.Map != null) {
         mowner.Map._Replace (mowner, mowner [0]);
    }
    if (destination.Map.Count == 1)
        builGroup = false;
        
     PListColumnMap owner = destination.Map;
     moved.RowIndex = destination.RowIndex;
     moved.ColIndex = destination.ColIndex;
     IPColumn column = moved;

     bool inserRow = false;
     if (builGroup) {
        //create new map
        PListColumnMap map = new PListColumnMap ();
        map.RowIndex = destination.RowIndex;
        map.ColIndex = destination.ColIndex;
            
        if (anch == AnchorStyles.Top) {
            map._Add (destination);
            map._Insert (moved, true);
        } else if (anch == AnchorStyles.Bottom) {
            map._Add (moved);
            map._Insert (destination, true);
        } else if (anch == AnchorStyles.Left) {
            map._Add (moved);
            map._Add (destination);
        } else if (anch == AnchorStyles.Right) {
            map._Add (destination);
            map._Add (moved);
        }
        column = map;
    } else {
        moved.RowIndex = destination.RowIndex;
        moved.ColIndex = destination.ColIndex;
        //move only by change indexes
        if (anch == AnchorStyles.Right)
            moved.ColIndex++;
        else if (anch == AnchorStyles.Top)
            inserRow = true;
        else if (anch == AnchorStyles.Bottom) {
            inserRow = true;
            moved.RowIndex++;
        }
    }
    owner._Insert (column, inserRow);
    TopMap.Info.OnBoundChanged (_cacheArg);
}

Properties Editor

ais.ctrl.FList

  • Inline properties (object.Property1.Property2...)  
  • Sorting. Grouping. Selecting. Editing
  • Printing (not implemented)
  • Save configuration with reference to item type (not implemented)

This class derived from PList and override several methods. It simply creates a list of Fields from properties from passed object and shows it in two columns: "Header" and "Value". And have 3rd column "Group" used for grouping. 

public class FList : PList
{
    protected object _fDataSource;
    protected Type _fDataType;
    protected FieldList _root = new FieldList ();
    protected bool _hideEmpty = false;
    protected FListState _state = FListState.Edit;
    protected PropertyChangedEventHandler _rowHandler;
    protected bool hideCollection;
    PListColumn colHeader;
    PListColumn colValue;
    PListColumn colGroup;

    public FList ()
        : base()
    {
        //used for autosize last column
        _sizeWidthOnCell = true;
        //edit mode
        _editMode = PListEditMode.ByClick;
        //config column generator option  
        this._columnAutoCreate = false;
        this._columnAutoToString = true;
        this._columnAutoToStringSort = true;
        this._headerVisibleHeader = false;
        this._contextMenu = new ContextMenuStrip ();
        
        //set base datasource by List[Fields]
        base.DataSource = _root;
        
        //init header column
        colHeader = (PListColumn)_info.Columns ["ToString"];
        colHeader.Editable = false;
        //init value column
        colValue = GeneratePropertyColumn ("Value");
        //init group column
        colGroup = GeneratePropertyColumn ("FieldGroup");
        colGroup.Visible = false;

        //config 
        this._info.ColumnsVisible = false;
        this._info.Columns.Add (colValue);
        this._info.Columns.Add (colGroup);
        this._info.TreeMode = true;
        this._info.HeaderVisible = false;
        this._info.GroupVisible = false;

        //new item changed handler
        _rowHandler = new PropertyChangedEventHandler (OnPropertyChanged);
    }
    ...

Background

This is planned as a universal controls library. The development was started on a database and documents flow projects.

It was a long way to implement my own control. Work started on a list view. The first step was control based on DataGridView in Virtual mode, but it used too many memory. To avoid this, the second step was to derive from ListView, but leak of columns configuration and grouping in virtual mode. So when I had enough time and looked on CodeProject, I decided that is possible. Hope this article helps everyone who wants to develop their own controls!

Many thanks to the entire CodeProject community. I take a lot of concepts, code from this site, and this article is the way in which I can bring it back.

Using the Code

Initialize Localization and List Information

// load data on startup 
Localize.Load ();
//or use
//ais.tool.Serialization.Load(Localize.Data, 
//  Path.Combine(Environment.CurrentDirectory, "localize.xml"))
Application.Run (new FormTest ());
//and save on exit
Localize.Save ();

Using Localize

public void Localizing ()
{
 this.button1.Text = Localize.Get ("Test", "List Editor");
 this.button2.Text = Localize.Get ("Test", "Test Reflection");
 this.button2.ToolTipText = Localize.Get ("Test", 
    "Test Reflection Accesor\nCompare properties access speed");
 this.button4.Text = Localize.Get ("Test", "Option Editor");
}

Using Docking Window

//initialize three controls
Form f = new Form();//main form
DockControl c = new DockControl();//container control
ReachTextBox r = new ReachTextBox();//simple editing control

//simple add it to your form
f.Controls.Add(c);

//and now add edit control to container, for example to left side
c.Add(r,DockType.Left);

Using PList

//to create list like System.Windows.Form.ListBox: 
PList list = new PList();
list.DataSource = _listDataSource
//now ready to use with all PList futures	    
//extended properties
list.AutoGenerateColumn = false;
list.AutoGenerateToString = true;
list.AutoGenerateToStringSort = true;
 //columns info
list.Info.HeaderVisible = false;
list.Info.ColumnsVisible = false;
list.Info.TreeMode = true;
//style setting            
list.StyleCell.Default.BorderColor = Color.Transparent;
list.StyleCell.Alter.BackColor = list.StyleCell.Default.BackColor;
list.StyleCell.Alter.BorderColor = list.StyleCell.Default.BorderColor;
 /custom properties 
list.SelectedItem = value;
...

History

  • 2011.09.16
    • First post
  • 2011.09.23
    • Default styles change
    • Column editing (move, sizing highlighting and auto grouping)
    • Column context menu (allow adding sub columns and show hided columns)
    • Indexed properties
    • Selection by mouse
    • Inline sub properties for PList (like object.Prop1.Prop2...)
    • Support System.Data.DataView (ais.ctrl.PDataList)
    • Test for PDataList
  • 2011.10.05
    • Debug mostly
    • Styles (completely changed, allow save and edit by user, on each column)
    • ToolForm behavior on Windows (TODO: debug in mono)
    • CellEditorFont and CellEditorColor

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

alexandrvslv



Kazakstan Kazakstan

Member



Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralMy vote of 1 PinmemberToli Cuturicu3:29 24 Sep '11  
GeneralRe: My vote of 1 PinPopularmemberalexandrvslv18:03 25 Sep '11  
GeneralRe: My vote of 1 PinmemberPaul Selormey18:16 25 Sep '11  
GeneralRe: My vote of 1 PinmemberToli Cuturicu22:14 25 Sep '11  
GeneralMy vote of 5 PinmemberPaul Selormey16:41 23 Sep '11  
GeneralRe: My vote of 5 Pinmemberalexandrvslv18:06 25 Sep '11  

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Mobile
Web04 | 2.5.120517.1 | Last Updated 5 Oct 2011
Article Copyright 2011 by alexandrvslv
Everything else Copyright © CodeProject, 1999-2012
Terms of Use
Layout: fixed | fluid