I'm trying to create a Bindable FlowLayoutPanel
that can take a DataSource
and map it the specified generic parameter <T>
controls. Only minimal requirements are to have a selectable index and to be able to change the background color of the selected and deselected controls. I've tried to leave out the implementation of the selected index except for obvious cases. This leaves the option to the developer as to how to move the selected index when a control is added/removed/etc. I have an example program that uses the below and it seems to work ok (it's not very large, but too big to post here).
I was hoping someone else could take a look at this and see if there are any obvious issues.
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Design;
namespace DataBindingListOfUserControls
{
/// <summary>
/// Create a Bindable FlowLayoutPanel
/// </summary>
/// <typeparam name="T">The control type that will be in the FlowLayoutPanel</typeparam>
/// <remarks>Note!!! If you wish to use this in a designer, you must create a class
/// that does not use or derive from this generic class. For example, doing something
/// like: BindableFlowLayoutPanel[MyCtrl] would give you designer errors. To fix this,
/// you need to create a class that does not derive from the generic class. So you
/// would do this: class MyCtrlBflp : MiddleBflp,
/// then class MiddleBflp : BindableFlowLayoutPanel[MyCtrl] Then you would use
/// MyCtrlBflp from the Toolbox (and you can create an instance of this through the
/// designer)</remarks>
public partial class BindableFlowLayoutPanel<T> : UserControl, INotifyPropertyChanged
where T : Control, IBindableObject, new()
{
#region Constants
/// <summary>
/// The constant value if no control is selected
/// </summary>
private const int NO_SELECTED_INDEX = -1;
#endregion
#region Member Variables
/// <summary>
/// The data source that will bind to the controls
/// </summary>
private BindingSource mDataSource = null;
/// <summary>
/// The deselected background color of the controls
/// </summary>
private Color mDeselectedControlColor = SystemColors.Control;
/// <summary>
/// The selected background color of the controls
/// </summary>
private Color mSelectetedControlColor = SystemColors.GradientInactiveCaption;
/// <summary>
/// The selected control within the FlowLayoutPanel
/// Let the model control this so you can implement your own desired
/// navigation when items are added, removed, inserted, etc
/// </summary>
private int mSelectedIndex = NO_SELECTED_INDEX;
#endregion
#region Constructor
/// <summary>
/// Creates an instance of the Bindable FlowLayoutPanel
/// </summary>
public BindableFlowLayoutPanel()
{
InitializeComponent();
}
#endregion
#region Properties
/// <summary>
/// Set the data source that will bind to the controls in the FlowLayoutPanel
/// </summary>
/// <param name="bindingSource">The binding source</param>
[TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")]
[Editor("System.Windows.Forms.Design.DataSourceListEditor,
System.Design", typeof(UITypeEditor))]
[AttributeProvider(typeof(IListSource))]
[DefaultValue(null)]
public object DataSource
{
set
{
// Only change if its different
if (mDataSource != value)
{
// Unregister the events if we were already bound to a source
CleanUpDataSource();
// Check to make sure the object is a data source
BindingSource new_binding_source = value as BindingSource;
if ((value != null) && (new_binding_source == null))
{
throw new ArgumentException("DataSource must be a BindingSource");
}
// Setup the binding source to listen for list changes and Enter event
// listeners for each control
mDataSource = new_binding_source;
if (mDataSource != null)
{
mDataSource.ListChanged +=
new ListChangedEventHandler(DataSourceListChangedEventHandler);
for (int i = 0; i < mDataSource.Count; i++)
{
AddControl(i);
}
}
}
}
}
/// <summary>
/// Get/Set the deselected background color of the controls
/// </summary>
public Color DeselectedControlColor
{
get
{
return mDeselectedControlColor;
}
set
{
mDeselectedControlColor = value;
}
}
/// <summary>
/// Get/Set the selected background color of the controls
/// </summary>
public Color SelectetedControlColor
{
get
{
return mSelectetedControlColor;
}
set
{
mSelectetedControlColor = value;
}
}
/// <summary>
/// Get/Set the currently selected control index
/// Make this bindable and let the outside world control
/// how this is updated
/// <remarks>The selected index will be internally set to
/// -1 if the selected index is removed or if a control
/// is attempted to be selected but fails</remarks>
/// </summary>
[System.ComponentModel.Bindable(true)]
public int SelectedIndex
{
get
{
return mSelectedIndex;
}
set
{
if (mSelectedIndex != value)
{
// Clear the currently selected control
ClearSelectedControl();
mSelectedIndex = value;
// Set the selected control
if (SetSelectedControl(value))
{
// Notify this change since its custom
NotifyPropertyChanged("SelectedIndex");
}
else
{
// Dont notify the reset and set internally as not set
mSelectedIndex = NO_SELECTED_INDEX;
}
}
}
}
#endregion
#region Functions
/// <summary>
/// Add a new control at the specified index
/// </summary>
/// <param name="indexOfControl">The index to add/insert the new
/// control</param>
private void AddControl(int indexOfControl)
{
// Create a new control
T new_control = new T();
// Setup the bindings from the UI to the data source
new_control.BindingObject = mDataSource[indexOfControl];
// Keep track of when the control is entered
new_control.Enter += new EventHandler(ControlEnteredEventHandler);
// Update the background color
new_control.BackColor =
(SelectedIndex == indexOfControl) ?
SelectetedControlColor : DeselectedControlColor;
// Add the new control to the end (no insert option, so add to end
// and then set the child index)
mFlowLayoutPanel.Controls.Add(new_control);
// Change the location of the control to the index desired
mFlowLayoutPanel.Controls.SetChildIndex(new_control, indexOfControl);
}
/// <summary>
/// Remove listeners from the data source and from the individual controls
/// Also call on Dispose
/// </summary>
private void CleanUpDataSource()
{
if (mDataSource != null)
{
// Unregister for list change events and remove the Enter event
// listeners for each control
mDataSource.ListChanged -=
new ListChangedEventHandler(DataSourceListChangedEventHandler);
while (mFlowLayoutPanel.Controls.Count > 0)
{
RemoveControl(0);
}
// No selected controls
SelectedIndex = NO_SELECTED_INDEX;
}
}
/// <summary>
/// Clear the selected control (Change background color to deselected)
/// </summary>
private void ClearSelectedControl()
{
if (SelectedIndex != NO_SELECTED_INDEX)
{
T last_ui_control_selected = GetControl(SelectedIndex);
if (last_ui_control_selected != null)
{
last_ui_control_selected.BackColor = DeselectedControlColor;
}
}
}
/// <summary>
/// Get the control at the specified index from the FlowLayoutPanel
/// </summary>
/// <param name="indexOfControl">The index of the control to get</param>
/// <returns>The control at the specified index. null if the control does
/// not exist</returns>
private T GetControl(int indexOfControl)
{
// Make sure the index is valid, otherwise return null
if (indexOfControl >= mFlowLayoutPanel.Controls.Count)
{
return null;
}
return mFlowLayoutPanel.Controls[indexOfControl] as T;
}
/// <summary>
/// Fire the property changed event with the specified property name
/// </summary>
/// <param name="propName">The property name that was updated</param>
private void NotifyPropertyChanged(String propName)
{
PropertyChangedEventHandler prop_changed = PropertyChanged;
if (prop_changed != null)
{
prop_changed(this, new PropertyChangedEventArgs(propName));
}
}
/// <summary>
/// Remove the control at the specified index
/// </summary>
/// <param name="indexOfControl">The index of the control to remove
/// from the FlowLayoutPanel</param>
private void RemoveControl(int indexOfControl)
{
// Get the control to remove from the FlowLayoutPanel
T control_to_remove = mFlowLayoutPanel.Controls[indexOfControl] as T;
// Unregister the Enter event
control_to_remove.Enter -= new EventHandler(ControlEnteredEventHandler);
control_to_remove.BindingObject = null;
// Remove the control from the FlowLayoutPanel
mFlowLayoutPanel.Controls.Remove(control_to_remove);
if (mSelectedIndex == indexOfControl)
{
// Internally clear the selected because the control was removed
mSelectedIndex = NO_SELECTED_INDEX;
}
}
/// <summary>
/// Set the specified control as selected
/// </summary>
/// <param name="indexOfControl">The index of the control
/// in the FlowLayoutPanel</param>
/// <returns>True if the control was selected, False otherwise</returns>
private bool SetSelectedControl(int indexOfControl)
{
if (indexOfControl >= 0)
{
T control_to_select = GetControl(indexOfControl);
if (control_to_select != null)
{
control_to_select.Select();
control_to_select.BackColor = SelectetedControlColor;
mFlowLayoutPanel.ScrollControlIntoView(control_to_select);
return true;
}
}
return false;
}
#endregion
#region Event Handlers
/// <summary>
/// Handle when a control in the FlowLayoutPanel is entered
/// </summary>
/// <param name="sender">The control that fired the event</param>
/// <param name="e">The event arguments</param>
private void ControlEnteredEventHandler(object sender, EventArgs e)
{
// Keep track of the currently selected index since this could be user fired
SelectedIndex = mFlowLayoutPanel.Controls.IndexOf(sender as Control);
}
/// <summary>
/// Handle when the data source has changed. Update the UI accordingly
/// </summary>
/// <param name="sender">The control that fired the event</param>
/// <param name="e">The event arguments</param>
private void DataSourceListChangedEventHandler(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.ItemAdded)
{
// Add the new control to the UI at the end that maps to the new model added
AddControl(e.NewIndex);
}
else if (e.ListChangedType == ListChangedType.ItemDeleted)
{
// Make sure the new index is ok to be deleted
if (mFlowLayoutPanel.Controls.Count > e.NewIndex)
{
// Remove the control from the UI at the specified index
RemoveControl(e.NewIndex);
}
}
}
#endregion
#region INotifyPropertyChanged Members
/// <summary>
/// The Property Changed event
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
} // BindableFlowLayoutPanel
} // DataBindingListOfUserControls
Here is the IBindableObject
definition:
public interface IBindableObject
{
object BindingObject { set; }
}
1 Answer 1
Turns out I was going about this the wrong way. I updated this to use the CurrencyManager
class instead to manage the bindings for me. This was a much better solution.
-
1\$\begingroup\$ It would be useful to edit your question or this answer to include the change, so anyone searching for the same information can see what the solution was. \$\endgroup\$Dan Lyons– Dan Lyons2012年04月04日 17:02:03 +00:00Commented Apr 4, 2012 at 17:02
-
\$\begingroup\$ @DanLyons - Fair enough, if anyone shows interest or ask questions I'll give more detail. Posted for over a month and not even a comment or answer, so I dont think anyones interested. Hopefully pointing out what I added will help anyone if they decide to do something similar. I had never heard of the CurrencyManager. \$\endgroup\$SwDevMan81– SwDevMan812012年04月04日 17:35:44 +00:00Commented Apr 4, 2012 at 17:35
-
2\$\begingroup\$ I'd be interested... I know it was a long time ago, but it was an interesting idea. Id love to know how it was ultimately solved. \$\endgroup\$RubberDuck– RubberDuck2016年01月22日 00:42:38 +00:00Commented Jan 22, 2016 at 0:42