I have a list of classes, but different children have different properties that need to be displayed.
What I want to achieve is to have a listbox-type control in the gui which enables each child to display it's properties the way it wants to - so not using the same pre-defined columns for every class.
I envisage something like the transmission interface (below), where each class can paint it's own entry, showing some text, progress bar if relevant, etc.
How can this be achieved in C#?
Thanks for any help.
Let your list items implement an interface that provides everything needed for the display:
public interface IDisplayItem
{
event System.ComponentModel.ProgressChangedEventHandler ProgressChanged;
string Subject { get; }
string Description { get; }
// Provide everything you need for the display here
}
The transmission objects should not display themselves. You should not mix domain logic (business logic) and display logic.
Customized ListBox:
In order to do display listbox items your own way, you will have to derive your own listbox control from System.Windows.Forms.ListBox. Set the DrawMode property of your listbox to DrawMode.OwnerDrawFixed or DrawMode.OwnerDrawVariable (if the items are not of the same size) in the constructor. If you use OwnerDrawVariable then you will have to override OnMeasureItem as well, in order to tell the listbox the size of each item.
public class TransmissionListBox : ListBox
{
public TransmissionListBox()
{
this.DrawMode = DrawMode.OwnerDrawFixed;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
if (e.Index >= 0 && e.Index < Items.Count) {
var displayItem = Items[e.Index] as IDisplayItem;
TextRenderer.DrawText(e.Graphics, displayItem.Subject, e.Font, ...);
e.Graphics.DrawIcon(...);
// and so on
}
e.DrawFocusRectangle();
}
}
You can let your original transmission class implement IDisplayItem or create a special class for this purpose. You can also have different types of objects in the list, as long as they implement the interface. The point is, that the display logic itself is in the control, the transmission class (or whatever class) only provides the information required.
Example:
Because of the ongoing discussion with Mark, I have decided to include a full example here. Let's define a model class:
public class Address : INotifyPropertyChanged
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
if (_Name != value) {
_Name = value;
OnPropertyChanged("Name");
}
}
}
private string _City;
public string City
{
get { return _City; }
set
{
if (_City != value) {
_City = value;
OnPropertyChanged("City");
OnPropertyChanged("CityZip");
}
}
}
private int? _Zip;
public int? Zip
{
get { return _Zip; }
set
{
if (_Zip != value) {
_Zip = value;
OnPropertyChanged("Zip");
OnPropertyChanged("CityZip");
}
}
}
public string CityZip { get { return Zip.ToString() + " " + City; } }
public override string ToString()
{
return Name + "," + CityZip;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Here is a custom ListBox:
public class AddressListBox : ListBox
{
public AddressListBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
ItemHeight = 18;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
const TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
if (e.Index >= 0) {
e.DrawBackground();
e.Graphics.DrawRectangle(Pens.Red, 2, e.Bounds.Y + 2, 14, 14); // Simulate an icon.
var textRect = e.Bounds;
textRect.X += 20;
textRect.Width -= 20;
string itemText = DesignMode ? "AddressListBox" : Items[e.Index].ToString();
TextRenderer.DrawText(e.Graphics, itemText, e.Font, textRect, e.ForeColor, flags);
e.DrawFocusRectangle();
}
}
}
On a form, we place this AddressListBox and a button. In the form, we place some initializing code and some button code, which changes our addresses. We do this in order to see, if our listbox is updated automatically:
public partial class frmAddress : Form
{
BindingList<Address> _addressBindingList;
public frmAddress()
{
InitializeComponent();
_addressBindingList = new BindingList<Address>();
_addressBindingList.Add(new Address { Name = "Müller" });
_addressBindingList.Add(new Address { Name = "Aebi" });
lstAddress.DataSource = _addressBindingList;
}
private void btnChangeCity_Click(object sender, EventArgs e)
{
_addressBindingList[0].City = "Zürich";
_addressBindingList[1].City = "Burgdorf";
}
}
When the button is clicked, the items in the AddressListBox are updated automatically. Note that only the DataSource of the listbox is defined. The DataMember and ValueMember remain empty.
yes, if you use WPF it is quite easy to do this. All you have to do is make a different DataTemplate for your different types.
MSDN for data templates
Dr. WPF for Items Control & Data Templates
Related
I have the class TestClass that has ToString overriden (it returns Name field).
I have instances of TestClass added into ListBox and at certain point I need to change Name of one of this instances, how then I can refresh it's text in ListBox?
using System;
using System.Windows.Forms;
namespace TestListBox
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
listBox1.Items.Add(new TestClass("asd"));
listBox1.Items.Add(new TestClass("dsa"));
listBox1.Items.Add(new TestClass("wqe"));
listBox1.Items.Add(new TestClass("ewq"));
}
private void button1_Click(object sender, EventArgs e)
{
((TestClass)listBox1.Items[0]).Name = "123";
listBox1.Refresh(); // doesn't help
listBox1.Update(); // same of course
}
}
public class TestClass
{
public string Name;
public TestClass(string name)
{
this.Name = name;
}
public override string ToString()
{
return this.Name;
}
}
}
try
listBox1.Items[0] = listBox1.Items[0];
I have encountered this same issue and tried all sorts of different ways to tr y to get the displayed text of an item to actually reflect the underlying item value.
After going through all the available properties I found this to be the simplest.
lbGroupList.DrawMode = DrawMode.OwnerDrawFixed;
lbGroupList.DrawMode = DrawMode.Normal;
It triggers the appropriate events within the control to update the displayed text.
Your Testclass needs to implement INotifyPropertyChanged
public class TestClass : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name;}
set
{
_name = value;
_notifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void _notifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public TestClass(string name)
{
this.Name = name;
}
public override string ToString()
{
return this.Name;
}
}
However this only works if you use Columns that do not rely on the ToString() but bind the property Name
This can be done by altering your code:
somewhere in class declare
BindingList<TestClass> _dataSource = new BindingList<TestClass>();
In initializeComponent write
listBox1.DataSource = _dataSource;
Then do all operations on _dataSource instead of Listbox.
You could use a BindingList:
items = new BindingList<TestClass>( );
listBox1.DataSource = items;
listBox1.DisplayMember = "_Name";
Then to refresh the list call:
items.ResetBindings( );
edit: Also don't forget to create a get Property for Name
public string _Name
{
get { return Name; }
set { Name= value; }
}
I use the following code:
public static void RefreshItemAt (ListBox listBox, int itemIndex)
{
if (itemIndex >= 0)
{
Rectangle itemRect = listBox.GetItemRectangle(itemIndex);
listBox.Invalidate(itemRect);
listBox.Update();
}
}
I do have a WPF binding question here.
Following Setup:
I do have a class (ActionService) having a name and a ObservableCollection of subitems (also a class named Step). A Step has a flag that shows if the Step is allready done (IsDone).
I bind a form to the ActionService and display all kind of things.
Everything works as aspected and i have just the essential parts in my snippet.
Now I need one more thing that i can not get work. I want the ActionService to know by binding how many of its Steps are open (IsDone == false). I you open a childform with one of the steps and change the IsDone-State, the mother form should get the new count on the fly.
And I'm to dumb to get a correct solution on the way ;-)
Thanks for your help or a best practise.
public class ActionService : BaseObject
{
public ActionService()
{
}
private String name;
public String Name
{
get { return this.name; }
set
{
this.name = value;
raisePropertyChanged("Name");
}
}
public ObservableCollection<Step> actionsteps;
public ObservableCollection<Step> ActionSteps
{
get { return this.actionsteps; }
set
{
this.actionsteps = value;
raisePropertyChanged("ActionSteps");
}
}
}
public class Step : BaseObject
{
public Step()
{
}
private String description;
public String Description
{
get { return this.description; }
set
{
this.description = value;
raisePropertyChanged("Description");
}
}
private Boolean isdone;
public Boolean IsDone
{
get { return this.isdone; }
set
{
this.isdone = value;
raisePropertyChanged("IsDone");
}
}
}
public class BaseObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void raisePropertyChanged(String parPropertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(parPropertyName));
}
}
}
You can create a new property in your ActionService class:
public bool IsDone
{
get
{
return ActionSteps.Count(x => x.IsDone) == ActionSteps.Count;
}
}
If the count of Steps in the ActionSteps list where the IsDone property is true is equal to the number of Steps in the ActionSteps list, then return true, else, return false.
To subscribe to the Steps property changed event, when you add an item to the collection, you simply need to subscribe to the PropertyChanged event:
//Create the item and subscribe to propertychanged.
Step item = new Step();
item.PropertyChanged += item_PropertyChanged;
//Add the item to the list.
ActionSteps.Add(item);
And your method will look like this:
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsDone")
raisePropertyChanged("IsDone");
}
I am using a poco class for the following screen but im just wondering how I would achieve the move up and down elements of this screen
I am using ObservableCollection to add my items to a mutual list my question is how would I achieve the move up and move down. I no I would need to change the poco class in real time but not sure how I would achieve this
private void AddColumn(object sender, RoutedEventArgs e)
{
if (this.WizardData == null)
return;
if (this.WizardData.ConcreteCustomColumnsProxy == null)
this.WizardData.ConcreteCustomColumnsProxy = new ObservableCollection<CustomColumnsModel>();
this.WizardData.ConcreteCustomColumnsProxy.Add(new CustomColumnsModel() { CustomColumnsDisplayName = txtDsiplayName.Text
, CustomColumnsOrder = 1, CustomColumnsWidth = Convert.ToInt32(txtWdith.Text) });
this.listView1.ItemsSource = this.WizardData.ConcreteCustomColumnsProxy;
this.listView1.UnselectAll();
this.listView1.Items.Refresh();
My Poco class is as follows
public event PropertyChangedEventHandler PropertyChanged;
public const string IdPropertyName = "CustomColumnsID";
private Guid _Id = Guid.Empty;
public Guid CustomColumnsID
{
get { return _Id; }
set
{
if (_Id == value)
return;
_Id = value;
NotifyPropertyChanged(IdPropertyName);
}
}
public string CustomColumnsDisplayName { get; set; }
public int CustomColumnsWidth { get; set; }
public int CustomColumnsOrder { get; set; }
protected void NotifyPropertyChanged(string key)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(key));
}
}
public EnterpriseManagementObject ActualData { get; private set; }
}
You have some sort of DataGrid control. You need to data bind a collection property to the DataGrid.ItemsSource property and a property of the same type as the items in the collection to the DataGrid.SelectedItems property:
<DataGrid ItemsSource="{Binding YourCollectionProperty}"
SelectedItem="{Binding YourItemProperty}" />
With the DataGrid.SelectedItems property data bound to your YourItemProperty, you can set which item is selected in the UI by setting this property. So to move the selected item down one position, you could do something like this:
int selectedIndex = YourCollectionProperty.IndexOf(YourItemProperty);
if (YourCollectionProperty.Count > selectedIndex)
YourItemProperty = YourCollectionProperty.ElementAt(selectedIndex + 1);
So that is how you perform the actions of the 'Move Down' Button and the 'Move Up' Button would work similarly. Then all you would need to do is to hook up some Click or ICommand event handlers.
I'm attempting to do what I considered simple data binding between a BindingSource and a ComboBox. I run into issues when the class I am using as the DataSource of the BindingSource has a property that is an instance of a generic class.
I have the following generic class:
public class GenericClass<T>
{
public T Code { get; set; }
public string Description { get; set; }
public override string ToString()
{
return Description;
}
}
I have a class that has an integer Code:
public class IntegerClass : GenericClass<int>
{
// Nothing unique here, for simple test.
}
I also have the class that is set to the BindingSource's DataSource:
public class ClassBindingClass : INotifyProperty Changed
{
private int _id;
private IntegerClass _choice;
private string _name;
public int Id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("Id");
}
}
public IntegerClass Choice
{
get { return _choice; }
set
{
_choice = value;
OnPropertyChanged("Choice");
}
}
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertName));
}
}
On my form I create a collection of IntegerClass and set my combobox's datasource as that collection. (This part works fine, the combo box displays the values appropriately.) Then I set the combobox's SelectedValue Binding to the BindingSource's Choice property updating on OnPropertyChanged.
If I replace IntegerClass with a non-generic class when you select a value in the combo box the BindingSource's Choice property changes the NotifyPropertyChanged event is fired and on my form I can update a label saying "Choice has changed!".
When the IntegerClass is part of the ClassBindingClass this no longer works and instead I cannot navigate out of the combo box and instead get a FormatException.
Is what I want to do possible? Can databinding handle generics?
You mention SelectedValue... but your source (and the bound property) are both IntegerClass - so it isn't a value you want to bind, but the item itself. Unfortunately, there is no ComboBox.SelectedItemChanged so you might need to hack it a bit to get 2-way binding...
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
IntegerClass[] choices = new[] {
new IntegerClass { Code = 123, Description = "a b c"},
new IntegerClass { Code = 456, Description = "d e f"},
new IntegerClass { Code = 789, Description = "g h i"},
};
ComboBox cbo = new TwoWayComboBox();
cbo.DropDownStyle = ComboBoxStyle.DropDownList;
cbo.DataSource = choices;
Form form = new Form();
ClassBindingClass obj = new ClassBindingClass();
cbo.DataBindings.Add("SelectedItem", obj, "Choice", true, DataSourceUpdateMode.OnPropertyChanged);
form.DataBindings.Add("Text", obj, "Choice", true, DataSourceUpdateMode.OnPropertyChanged); // show it
form.Controls.Add(cbo);
Application.Run(form);
}
}
class TwoWayComboBox : ComboBox {
public new object SelectedItem
{
get { return base.SelectedItem; }
set { base.SelectedItem = value; }
}
private static readonly object SelectedItemChangedKey = new object();
public event EventHandler SelectedItemChanged {
add { Events.AddHandler(SelectedItemChangedKey, value);}
remove { Events.RemoveHandler(SelectedItemChangedKey, value);}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
EventHandler handler = (EventHandler)Events[SelectedItemChangedKey];
if (handler != null) { handler(this, EventArgs.Empty); }
base.OnSelectedIndexChanged(e);
}
}
I have created my own radio button class – namely MyRadioButton, as the built in .NET class did not enlarge effectively. (using this for touch screen)
MyRadioButton Class works well, expect for an issue which I do not know How to resolve - When I have multiple MyRdaioButtons on a form, I can select all of them.... They somehow do not work as they should where when one selects one the others are automatically be deselected.
My code is as follows:
public class MyRadioButton : Control
{
public MyRadioButton()
{
}
private string textTowrite;
private bool checkStatus;
private int width;
private int height;
public event EventHandler CheckedChanged;
public delegate void MyHandler1(object sender, EventArgs e);
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
if (Checked)
Checked = false;
else
Checked = true;
Invalidate(true);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
ButtonState btnstate;
Rectangle rRadioButton;
if (checkStatus)
{
btnstate = ButtonState.Checked;
}
else
btnstate = ButtonState.Normal;
rRadioButton = new Rectangle(0, 0, RBWidth, RBHeight);
FontFamily ft = new FontFamily("Tahoma");
Font fnt_radio = new Font(ft, (int)(18), FontStyle.Bold);
ControlPaint.DrawRadioButton(e.Graphics, -2, 10, rRadioButton.Width,
rRadioButton.Height, btnstate);
//RadioButton's text left justified & centered vertically
e.Graphics.DrawString(textTowrite, fnt_radio, new SolidBrush(Color.Black), rRadioButton.Right + 1, 16);
}
protected virtual void OnCheckedChanged(EventArgs e)
{
if (CheckedChanged != null)
{
CheckedChanged(this, e);
}
}
public override string Text
{
get { return textTowrite; }
set { textTowrite = value; }
}
public bool Checked
{
get { return checkStatus; }
set
{
checkStatus = value;
OnCheckedChanged(EventArgs.Empty);
}
}
public int RBWidth
{
get
{
if (width == 0)
{
width = 40;
}
return width;
}
set
{
if (width != value)
{
width = value;
Invalidate();
}
}
}
public int RBHeight
{
get
{
if (height == 0)
{
height = 40;
}
return height;
}
set
{
if (height != value)
{
height = value;
Invalidate();
}
}
}
}
If someone could provide me with a solution it would be greatly appreciated, as I am pulling out my hair
Thanks
Jens
You may also consider inheriting your control directly from RadioButton, giving you access to the RadioButton.GroupName property, or you will need to implement this type of functionality yourself as kbrinley has posted.
Have you considered using images on a RadioButton control instead? According to ButtonBase's documentation (which RadioButton inherits from):
To have the derived button control
display an image, set the Image
property or the ImageList and
ImageIndex properties.
Note that I have no idea how you'd do selected/unselected states with images... I imagine the ImageList is related to this.
Since this is your control, you will have to provide the logic for this to act like a radio button.
First, I'd suggest placing all of your Radio buttons into a Container control.
Then, at the beginning OnClick method of your control, use the GetContainerControl method to retrieve the Container object and iterate over all of the Radio buttons in the container and set the Checked property of them to false.