How do I change boolean properties with one click in PropertyGrid - c#

We have a windows form PropertyGrid that we use to display all the properties. We have drawn a checkbox on Boolean property that checks it self and unchecks itself based on the value. this all works fine.
the issue is, that user wants to change the check box value in single click, whereas property grid changes it on a double click and I cant figure out a way to handle clicks or change property value on single click when property type is Boolean.
How to change property value in single click?

PropertyGrid internally has methods which allows us to use them with reflection to get the GridItem under mouse when you click on its PropertyGridView internal control.
In below code, I handled mouse click on its PropertyGridView control and checked if the item under mouse position is a boolean property, I reversed it's value. The event will fire for the label of property, also for icon area of the property editor:
PropertyGrid
using System;
using System.Drawing;
using System.Reflection;
using System.Windows.Forms;
public class ExPropertyGrid : PropertyGrid
{
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
var grid = this.Controls[2];
grid.MouseClick += grid_MouseClick;
}
void grid_MouseClick(object sender, MouseEventArgs e)
{
var grid = this.Controls[2];
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var invalidPoint = new Point(-2147483648, -2147483648);
var FindPosition = grid.GetType().GetMethod("FindPosition", flags);
var p = (Point)FindPosition.Invoke(grid, new object[] { e.X, e.Y });
GridItem entry = null;
if (p != invalidPoint) {
var GetGridEntryFromRow = grid.GetType()
.GetMethod("GetGridEntryFromRow", flags);
entry = (GridItem)GetGridEntryFromRow.Invoke(grid, new object[] { p.Y });
}
if (entry != null && entry.Value != null) {
object parent;
if (entry.Parent != null && entry.Parent.Value != null)
parent = entry.Parent.Value;
else
parent = this.SelectedObject;
if (entry.Value != null && entry.Value is bool) {
entry.PropertyDescriptor.SetValue(parent,!(bool)entry.Value);
this.Refresh();
}
}
}
}
Drawing CheckBox in PropertyGrid
public class MyBoolEditor : UITypeEditor
{
public override bool GetPaintValueSupported
(System.ComponentModel.ITypeDescriptorContext context)
{ return true; }
public override void PaintValue(PaintValueEventArgs e)
{
var rect = e.Bounds;
rect.Inflate(1, 1);
ControlPaint.DrawCheckBox(e.Graphics, rect, ButtonState.Flat |
(((bool)e.Value) ? ButtonState.Checked : ButtonState.Normal));
}
}
Class which used in screenshot
public class Model
{
public int Property1 { get; set; }
[Editor(typeof(MyBoolEditor), typeof(UITypeEditor))]
public bool Property2 { get; set; }
[TypeConverter(typeof(ExpandableObjectConverter))]
public Model Property3 { get; set; }
}

I'd like to comment, but rep isn't high enough yet.
The accepted answer works great. However as mentioned the code doesn't trigger the PropertyValueChanged event.
Adding a call to OnPropertyValueChanged triggers the PropertyValueChanged event.
entry.PropertyDescriptor.SetValue(parent, !(bool)entry.Value);
this.Refresh();
base.OnPropertyValueChanged(null);
Then in the PropertyValueChanged event code you can access the custom object that has been changed.
To communicate the changed property back to the form create some properties in the custom object, with Browsable set to false so they do not appear in the PropertyGrid.
[Browsable(false)]
public string changedParent { get; set; }
[Browsable(false)]
public string changedLabel { get; set; }
[Browsable(false)]
public string changedValue { get; set; }
At the top of the Form class create this static property
public partial class Form1 : Form
{
private static Form1 form = null;
In the constructor of Form1 link form to this.
public Form1()
{
InitializeComponent();
..
..
form = this;
Back in grid_MouseClick before triggering OnPropertyValueChanged save off the changed property information.
entry.PropertyDescriptor.SetValue(parent, !(bool)entry.Value);
this.Refresh();
form.sh.changedParent = entry.Parent.Label;
form.sh.changedLabel = entry.Label;
form.sh.changedValue = entry.Value.ToString();
base.OnPropertyValueChanged(null);
Now in the PropertyValueChanged event code you can determine which property was changed.
form.customobject.changedParent
form.customobject.changedLabel
form.customobject.changedValue

The best answer used a reflection to get GridItem under the mouse. However, I don't see the point in doing this since it's enough to request a dedicated GridItem. Here is my implementation of MouseClick:
void grid_MouseClick(object sender, MouseEventArgs e)
{
GridItem entry = SelectedGridItem;
if (entry != null && entry.Value != null && entry.Value is bool b)
{
var obj = SelectedObjects.Length == 1 ? SelectedObject : SelectedObjects;
entry.PropertyDescriptor.SetValue(obj, !b);
}
}

The above code from Reza Aghaei work long time but now changed the list of controls.
void grid_MouseClick(object sender, MouseEventArgs e)
{
var grid = this.Controls[2]; //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var invalidPoint = new Point(-2147483648, -2147483648);
///// following line throws a Nullreference Exception
var FindPosition = grid.GetType().GetMethod("FindPosition", flags);
var p = (Point)FindPosition.Invoke(grid, new object[] { e.X, e.Y });
GridItem entry = null;
...
}
Now you need to select the right control(PropertyGridView).
Below my solution.
int idx = -1;
for (int i = 0; i < this.Controls.Count; i++)
{
Control control = this.Controls[i];
if (control.Text.Contains("PropertyGridView"))
{
idx = i;
break;
}
}
var grid = this.Controls[idx];

Related

Is there any reason not to create a class or struct to resolve a 'non boolean' boolean

I am just a hobby programmer but I come across may cases where I want to switch a type value (e.g. label.backgroundcolor) depending on whether it has been clicked or not, or whether the mouse is over or not. This is normally a trivial change but there has to be code for the event in each case and this then involves passing information such as default colour, mouse-over colour or default fontstyle, mouse-over fontstyle or any of many other types of 'switch'. In all cases it is simply a case of toggling the change between one value and another but, because this could potentially happen over several different types (Labels, Textboxes, Panels etc.) I find I have to code for each type separately.
Is there any good reason why I shouldn't just do this
class AnyObjectBoolean
{
private object objOne;
private object objTwo;
public AnyObjectBoolean(object oneValue, object twoValue)
{
objOne = oneValue;
objTwo = twoValue;
}
public object invert(Object val)
{
if (val.ToString() == objOne.ToString())
{
return objTwo;
}
else
{
return objOne;
}
}
I then create a new instance for each object and style I want to change and the resulting event code becomes (for instance)
private void Label_MouseClick(object sender, EventArgs e)
{
var Label = (Label)sender;
Label.BackColor = (Color)SelectColours.invert(Label.BackColor);
}
where SelectColours is an instance of AnyObjectBoolean.
Maybe not a great question but I ask because I've never found anything like this implemented anywhere.
Disclaimer:- this is my first post so I may not have tagged entirely appropriately or completely.
First of all welcome to StackOverflow.
Personally, I'd prefer to handle this through a catch-all function, in which I would deal with all the types of objects that I wanted. Something like:
private Color ToggleColor(object sender, Color currentColor)
{
Color labelBackColor = Color.White;
Color labelHoverColor = Color.Yellow;
Color textBackColor = Color.Wheat;
Color textHoverColor = Color.Turquoise;
Color defaultBackColor = Color.Tomato;
Color defaultHoverColor = Color.SteelBlue;
Label l = sender as Label;
if (l != null)
{
return currentColor == labelBackColor ? labelHoverColor : labelBackColor;
}
TextBox t = sender as TextBox;
if (t != null)
{
return currentColor == textBackColor ? textHoverColor : textBackColor;
}
return currentColor == defaultBackColor ? defaultHoverColor : defaultBackColor;
}
Doing it this way, I keep the entire color scheme in one place. BTW don't use my suggested colors! Note the use of "as". It does the same as cast, with the important difference that it does not throw an Exception if it fails, it simply returns null. Therefore, you can simply try each object in turn safely until you get a hit.
there is no need for an instance, as i understand your program static class will be more appropriate here and you can change it from outside whenever you need.
use one method that will add events to all controls (you can add click event to all control types not just a label).
I think you want to do something like that:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
MyColors.firstColor = Color.Blue;
MyColors.secondColor = Color.Yellow;
// add events to all requested controls
AddEvent(new Control[] {
label1,
label2,
label3,
});
}
public void AddEvent(Control[] myControls)
{
foreach (Control c in myControls)
{
c.MouseClick += Control_Click;
}
}
private void Control_Click(object sender, EventArgs e)
{
((Control)sender).BackColor = MyColors.GetColor(((Control)sender).BackColor);
}
}
static class MyColors
{
public static Color firstColor { get; set; } = Color.Black;
public static Color secondColor { get; set; } = Color.White;
public static Color GetColor(Color color)
{
if (color == firstColor)
{
return secondColor;
}
else
{
return firstColor;
}
}
}
Most of the controls such as TextBox, Label and many other controls inherit from the Control class. The property BackColor is a property of the Control class; therefore, you can write a method or a class that takes a Control type and changes its color. Below is a class which takes 2 colors in its constructors and then inverts the passed in Control instance from one color to another.
public class ColorInverter
{
public Color Color1 { get; private set; }
public Color Color2 { get; private set; }
public ColorInverter(Color color1, Color color2)
{
this.Color1 = color1;
this.Color2 = color2;
}
public void Invert(Control control)
{
if (control.BackColor == this.Color1)
{
control.BackColor = this.Color2;
return;
}
control.BackColor = Color1;
}
}
Usage:
ColorInverter c = new ColorInverter(Color.Black, Color.Red);
TextBox box = new TextBox();
c.Invert(box);
Here is the inheritance hierarchy for Label. You can look for other controls there as well.
You may even check if the control inherits Control before calling the Invert method:
if (box is Control)
{
c.Invert(box);
}
else
{
// ...do something
}
You can use access any properties such as FontSize, FontFamily and many other Control properties as well. Obviously change the class name from ColorInverter to something else if you want to change the FontSize etc. as well.

Setting default size/text of custom control in c#

I am creating a custom control in my C# application in order to add a new property (MyProperty below). It is inheriting from Label. One thing I would like it to do, is display at a particular size when I drag it on to my form (200x132). I'd also like it to display no text. However, no matter how I try to do this, it doesn't seem to work. I am able to set BackColor and BorderStyle with no problem, however. I'm fairly new to C#, so maybe I'm missing something obvious.
Here is my code:
using System.Drawing;
using System.Windows.Forms;
namespace MyProgram
{
public enum MyEnum
{
Value1, Value2, Value3
}
public partial class MyControl : Label
{
public MyControl()
{
BackColor = Color.LightCoral;
BorderStyle = BorderStyle.FixedSingle;
AutoSize = false;
Size = new Size(200, 132);
Text = "";
InitializeComponent();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
}
private MyEnum myProperty;
public MyEnum MyProperty
{
get { return myProperty; }
set { myPropery = value; }
}
}
}
The answer provided via Dispersia's link has a bug, in my opinion. The text reset should happen once and then whatever a user does after that shouldn't matter. In Dispersia's link you can't actually set the text back to the control name because it will keep blanking it out.
The answer provided by cramopy doesn't technically answer your question, it is a way to do it by using the defaults on a UserControl though. You'll also need to bind the Text property of the UserControl to the label's.
The following should work while inheriting from a Label and will only reset the Text property once.
public partial class MyControl : Label
{
#region fields
private IComponentChangeService _changeService;
private bool canResetText = false;
#endregion
#region properties
protected override Size DefaultSize
{
get { return new Size(200, 132); }
}
[Browsable(false)]
public override bool AutoSize
{
get { return false; }
set { base.AutoSize = false; }
}
public override ISite Site
{
get { return base.Site; }
set
{
base.Site = value;
if (!base.DesignMode)
return;
this._changeService = (IComponentChangeService)base.GetService(typeof(IComponentChangeService));
if (this._changeService != null)
this._changeService.ComponentChanged += new ComponentChangedEventHandler(this.OnComponentChanged);
}
}
#endregion
#region constructors
public MyControl()
{
base.BackColor = Color.LightCoral;
base.BorderStyle = BorderStyle.FixedSingle;
}
#endregion
#region methods
protected override void InitLayout()
{
base.InitLayout();
this.canResetText = true;
}
private void OnComponentChanged(object sender, ComponentChangedEventArgs ce)
{
if (ce.Component != null &&
ce.Component == this &&
ce.Member.Name == "Text" &&
base.DesignMode &&
this.canResetText)
{
((MyControl)ce.Component).Text = string.Empty;
this.canResetText = false;
if (this._changeService != null)
this._changeService.ComponentChanged -= new ComponentChangedEventHandler(this.OnComponentChanged);
}
}
#endregion
}
#Dispersia reply only answers the myControl1 thing. (deleted meanwhile)
Here comes a full guide for solving your problem:
Add a new UserControl named MyLabel
Change the following within Designer Mode:
BorderStyle:= FixedSingle
Size:= 200; 132
Now Drag&Drop a new Label onto the control
Edit those Label values (also within Designer Mode):
AutoSize:= false
BackColor:= LightCoral
Dock:= Fill
Text:= clear/empty this box!! (don't write this inside the box, you really have to clear it!)
TextAlign:= MiddleCenter
Just recompile your project && add a MyLabel control from the Toolbar.
Now it show up as you wanted!!

Custom group box not binding to bindingsource

I need to bind a GroupBox to a BindingSource, which in turn is bound to the following object:
public class CustomerType
{
public int Id {get; set;}
public string Name {get; set;}
public MemberType MemberType {get; set;}
}
public enum MemberType {Adult, Child}
I followed this answer to create a custom GroupBox. I also set the data bindings as follows:
groupBoxMemberType.DataBindings.Add("Selected", this.bindingSource, "MemberType");
However, when loading an existing object, I get the following exception:
DataBinding cannot find a row in the list that is suitable for all bindings.
The exception occurs when setting the data source:
customerType = customerTypeRequest.Load(id);
bindingSource.DataSource = customerType; //raises exception
What am I missing? Is there an alternative to get radio buttons to bind to a datasource, specifically a BindingSource?
This is the changed code:
[DefaultBindingProperty("Selected")]
public class RadioGroupBox : GroupBox
{
#region events
[Description("Occurs when the selected value changes.")]
public event SelectedChangedEventHandler SelectedChanged;
public class SelectedChangedEventArgs : EventArgs
{
public int Selected { get; private set; }
internal SelectedChangedEventArgs(int selected)
{
this.Selected = selected;
}
}
public delegate void SelectedChangedEventHandler(object sender, SelectedChangedEventArgs e);
#endregion
private int selected;
[Browsable(false)]
[Bindable(BindableSupport.Yes, BindingDirection.TwoWay)]
[Description("The selected value associated with this control."), Category("Data")]
public int Selected
{
get { return selected; }
set
{
int val = 0;
var radioButton = this.Controls.OfType<RadioButton>()
.FirstOrDefault(radio =>
radio.Tag != null
&& int.TryParse(radio.Tag.ToString(), out val) && val == value);
if (radioButton != null)
{
radioButton.Checked = true;
selected = val;
}
}
}
protected override void OnControlAdded(ControlEventArgs e)
{
base.OnControlAdded(e);
var radioButton = e.Control as RadioButton;
if (radioButton != null)
radioButton.CheckedChanged += radioButton_CheckedChanged;
}
protected void OnSelectedChanged(SelectedChangedEventArgs e)
{
if (SelectedChanged != null)
SelectedChanged(this, e);
}
private void radioButton_CheckedChanged(object sender, EventArgs e)
{
var radio = (RadioButton)sender;
int val = 0;
if (radio.Checked && radio.Tag != null
&& int.TryParse(radio.Tag.ToString(), out val))
{
selected = val;
OnSelectedChanged(new SelectedChangedEventArgs(selected));
}
}
}
Further to setting the Tag property to the corresponding int value of the enum, you need to subscribe to the SelectedChanged event in your form, eg:
private void radioGroupBoxMemberType_SelectedChanged(object sender, SelectedChangedEventArgs e)
{
customerType.MemberType = (MemberType)e.Selected;
}
Improvements to this class would be:
Inherit from RadioButton and use a new property instead of the Tag property.
Access and set the bindingsource property directly in the control to avoid subscribing to the event.

How to create user control for displaying collection of other user controls in WinForms?

I need to create a user control MyTypeListControl to display collection of objects of type MyType using a user controls MyTypeDisplayControl instance for each of those objects.
So that I could
add instance of MyTypeListControl to my WinForm, then
load collection of MyType and
assign it to MyTypeListControl's DataSource.
In the result it should generate and show appropriate count of MyTypeDisplayControl instances in MyTypeListControl's instance.
In case if I needed to show list of properties - equivalent would be DataGrid with specific fields from MyType assigned to specific DataGrid's columns, but I want to view each MyType item as a user control - with more power for visual representation and functionality than DataGrid provides for it's rows.
Is that even possible?
I found this SO resource how to create My collection type, but this is only small part of the problem solution...
It is quite easy (if you know how) and doesn't take so much effort as you might think in the first place (at least for a simple implementation that handles collection of less then 100 items).
So at first lets create a MyType:
public class MyType
{
public static MyType Empty = new MyType(String.Empty, DateTime.MinValue);
public MyType(string myName, DateTime myBirthday)
{
MyName = myName;
MyBirthday = myBirthday;
}
public DateTime MyBirthday { get; private set; }
public string MyName { get; private set; }
}
At next we need a MyTypeControl:
public partial class MyTypeControl : UserControl
{
private MyType _MyType;
private Label labelBirthday;
private Label labelName;
private Label labelSeparator;
public MyTypeControl()
{
InitializeComponent();
}
public event EventHandler MyTypeChanged;
public MyType MyType
{
get { return _MyType; }
set
{
if (_MyType == value)
return;
_MyType = value ?? MyType.Empty;
OnMyTypeChanged(EventArgs.Empty);
}
}
protected virtual void OnMyTypeChanged(EventArgs eventArgs)
{
UpdateVisualization();
RaiseEvent(MyTypeChanged, eventArgs);
}
protected void UpdateVisualization()
{
SuspendLayout();
labelName.Text = _MyType.MyName;
labelBirthday.Text = _MyType.MyBirthday.ToString("F");
labelBirthday.Visible = _MyType.MyBirthday != DateTime.MinValue;
ResumeLayout();
}
private void InitializeComponent()
{
labelName = new Label();
labelBirthday = new Label();
labelSeparator = new Label();
SuspendLayout();
labelName.Dock = DockStyle.Top;
labelName.Location = new Point(0, 0);
labelName.TextAlign = ContentAlignment.MiddleCenter;
labelBirthday.Dock = DockStyle.Top;
labelBirthday.TextAlign = ContentAlignment.MiddleCenter;
labelSeparator.BorderStyle = BorderStyle.Fixed3D;
labelSeparator.Dock = DockStyle.Top;
labelSeparator.Size = new Size(150, 2);
Controls.Add(labelSeparator);
Controls.Add(labelBirthday);
Controls.Add(labelName);
MinimumSize = new Size(0, 48);
Name = "MyTypeControl";
Size = new Size(150, 48);
ResumeLayout(false);
}
private void RaiseEvent(EventHandler eventHandler, EventArgs eventArgs)
{
var temp = eventHandler;
if (temp != null)
temp(this, eventArgs);
}
}
Then comes our magically list control:
public class MyTypeListControl : UserControl
{
private ObservableCollection<MyType> _Items;
public MyTypeListControl()
{
AutoScroll = true;
_Items = new ObservableCollection<MyType>();
_Items.CollectionChanged += OnItemsCollectionChanged;
}
public Collection<MyType> Items
{
get { return _Items; }
}
private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UpdateVisualization();
}
private void UpdateVisualization()
{
SuspendLayout();
Controls.Clear();
foreach (var item in _Items)
{
var control = new MyTypeControl { MyType = item, Dock = DockStyle.Top };
Controls.Add(control);
Controls.SetChildIndex(control, 0);
}
ResumeLayout();
}
}
And now simply create the list control in your form or parent control and fill it with some meaningful values:
myTypeListControl.Items.Add(new MyType("Adam", DateTime.UtcNow.Add(-TimeSpan.FromDays(365 * 40))));
myTypeListControl.Items.Add(new MyType("Eva", DateTime.UtcNow.Add(-TimeSpan.FromDays(365 * 38))));

Manipulating collection from the inside

I have a collection of panels which are highlighted when user clicks on them. I want to force them to behave as a set of radio buttons so only the one that is clicked on is highlighted and others aren't.
I guess that there must be a way to manipulate whole collection (set property to false) from the inside, because the event is triggered by one item from the collection. Is there a way for the one item to manipulate whole collection? This is such a common feature in applications so I guess there must be a pattern how to do it properly. Thanks.
You may store collection of your panels and handle required functionality as in following code snippet:
List<Panel> Panels;
private void Initialization()
{
Panels = new List<Panel>();
Panels.Add(pnl1);
Panels.Add(pnl2);
//add all your panels into collection
foreach(Panel Item in this.Panels)
{
//add handle to panel on click event
Item.Click += OnPanelClick;
}
}
private void OnPanelClick(object sender, EventArgs e)
{
foreach(Panel Item in this.Panels)
{
//remove highlight from your panels, real property should have other name than Panel.HighlightEnabled
Item.HighlightEnabled = false;
}
((Panel)sender).HighlightEnabled = true; //add highlight to Panel which invoked Click event
Application.DoEvents(); //ensure that graphics redraw is completed immediately
}
private void AddNewPanelIntoLocalCollection(Panel panel)
{
//here you can add new items to collection during program lifecycle
panel.Click += OnPanelClick;
this.Panels.Add(panel);
}
This is how I do it
public class SelectOne : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private bool isSelected = false;
private HashSet<SelectOne> selecteOnes = null;
public bool IsSelected
{
get { return isSelected; }
set
{
if (isSelected == value) return;
if (isSelected && selecteOnes != null)
{
foreach (SelectOne so in selecteOnes)
{
if (so == this) continue;
so.IsSelected = false;
}
}
NotifyPropertyChanged("IsSelected");
}
}
public SelectOne() { }
public SelectOne(bool IsSelected) { isSelected = IsSelected; }
public SelectedOne(bool IsSelected, HashSet<SelectOne> SelecteOnes)
{
isSelected = IsSelected;
selecteOnes = SelecteOnes;
}
}
Eventually I did find a way to do this properly with only one delegate.
In class A I have a collection of objects B
List<B> b = new List<B>
class B, needs to have an unique ID and delegete for void metod with Id parameter
delegate void DeleteItemDelegate(int id);
class B
{
public int ID {get; set;}
public DeleteItemDeleate deleteThis {set; get;}
}
class A has a metod like this:
public void RemoveItem(int id)
{
for (int x = 0; x < b.Count; x++)
{
if (b[x].id == id)
{
b.RemoveAt(x);
}
}
}
when adding a new B object into List just add metod RemoveItem to B.deleteThis delegate
B bObject = new B();
bObject.deleteThis = RemoveItem;
b.Add(bObject);
Now all you need to do is add DeleteMe metod in B class
void DeleteMe()
{
// and call local delegate - pointing to metod which actually can manipulate the collection
deleteThis(id);
}

Categories