Extend ListViewItem in WinForms - c#

I've made a fatal mistake.
I totally forgot to extend the ListView to include some important features. And now it's way too late to just delete the ListView, add those features and re-add the ListView all over again.
What I need to add is simple, and I've done it before, but I don't think I can do it the same way as before because when I did it before I did it before I created the ListView and placed it on the Form.
I need to be able to do:
ListViewItem item = new ListViewItem();
item.SomeNewProperty = "yay";
But how can I extend the functionality now that I've already created a ListView control and added it to the form and set all of its properties and setup all of the events. If I delete the ListView and extend it then re-add it everything will be lost and I'd have to start over again.
Is there a way to extend it to add that new property to ListViewItem without starting from scratch?

Each control in System.Windows.Form contain a property called Tag, that is an Object DataType, if you just want to save one value in each ListViewItem you should do the following:
ListViewItem Item = new ListViewItem();
Item.Tag = "Something";
But if you want to save multiple values in each ListViewItem you can create a class that contain all the info thet you want to set. Ex.
public class MyClass
{
public string Value1 {get; set;}
public string Value2 {get; set;}
}
MyClass oInfo = new MyClass();
oInfo.Value1 = "Smething";
oInfo.Value2 = "Is Good";
ListViewItem Item = new ListViewItem();
Item.Tag = oInfo;
To retrieve the object do the following:
MyClass oRetrieveInfo = (MyClass)LisView.Items[index].Tag;
MessageBox.Show(oRetrieveInfo.Value1 + " " + RetrieveInfo.Value2);

Related

Name dynamically generated combobox items

I am currently working on a WPF project in .NET Core 3.1. During the execution I have to read some file names to a combobox. I do that by first adding them to a list. After that I iterate through that list and add them to the combobox.
Problem
Because they should only be accessible if an Item from another combobox is selected, I need to disable the items I just added. Because of that, and to later enable them, I need to name them. But I could not figure our how to do that.
Here is my code:
string[] allAircraft;
allAircraft = Directory.GetFiles(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + #"\ApplicationName\Aircraft\", "*.lxml", SearchOption.TopDirectoryOnly);
foreach (var currentAircraft in allAircraft)
{
aircraft.Add(System.IO.Path.GetFileNameWithoutExtension(currentAircraft));
}
foreach (string currentListItem in aircraft)
{
comboAircraft.Items.Add(currentListItem);
}
foreach (ComboBoxItem currentComboItems in comboAircraft.Items)
{
currentComboItems.Name = "comboAircraftItems" + currentComboItems.Content;
}
At the line foreach (ComboBoxItem currentItems in comboAircraft.Items) I get the Exception:
System.InvalidCastException: 'Unable to cast object of type 'System.String' to type 'System.Windows.Controls.ComboBoxItem'.'
I am really running out of ideas. I googled one entire day but could not find anything.
Have a nice day everyone!
# t-kldw, Your task is very difficult to solve when working directly with UI elements.
But it is very simple to solve, if you separate the Data and their Presentation, as it should be in MVVM.
Unfortunately, what you wrote contains few details for an example with full implementation.
I can give only partial advice.
It is necessary to create an additional container-class for the data needed in one combo box.
This class MUST have an INotifyPropertyChanged implementation.
To simplify, take implementation from the topic OnPropertyChanged and instance property
/// <summary>Base class implementing INotifyPropertyChanged.</summary>
public abstract class BaseINPC : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>Called AFTER the property value changes.</summary>
/// <param name="propertyName">The name of the property.
/// In the property setter, the parameter is not specified. </param>
public void RaisePropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
An example class with two properties: File Name and IsEnabled to allow selection.
public class FileItem : BaseINPC
{
private string _fileName;
private bool _isEnabled;
public string FileName { get => _fileName; set { _fileName = value; RaisePropertyChanged(); } }
public bool IsEnabled { get => _isEnabled; set { _isEnabled = value; RaisePropertyChanged(); } }
}
So that I can give further advice, you need to provide more details of your task.
For example, I don’t even have a clue why currentComboItems.Name might be needed ....
I think that you are clearly doing something wrong.
You are trying to convert String to ComboBoxItem. When you populate your combobox with data, you add String, not ComboBoxItem. So you should use String in the last foreach loop.
foreach (string currentComboItems in comboAircraft.Items)
{
currentComboItems.Name = "comboAircraftItems" + currentComboItems;
}
Another solution is to add ComboBoxItem to your combobox instead of String.
foreach (string currentListItem in aircraft)
{
ComboBoxItem item = new ComboBoxItem()
{
Content = currentListItem,
Name = "comboAircraftItems" + currentListItem
};
comboAircraft.Items.Add(item);
}
ItemsControl.Items only holds data objects. In order to render this items, each is wrapped into a container e.g., ComboBoxItem. These UI containers are generated by a ItemContainerGenerator, which is associated with the current ItemsControl. The ItemContainerGenerator is also responsible to track the mapping between the data item and its UI container.
To obtain the container of an item you use the ItemsControl.ItemContainerGenerator:
DependencyObject itemContainer = ItemContainerGenerator.ContainerFromItem(item);
Your simplified code becomes:
List<string> allAircrafts = Directory.EnumerateFiles(
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
#"\ApplicationName\Aircraft\"),
"*.lxml",
SearchOption.TopDirectoryOnly)
.Select(Path.GetFileNameWithoutExtension)
.ToList();
comboAircraft.ItemsSource = allAircrafts;
foreach (string item in allAircrafts)
{
var itemContainer = comboAircraft.ItemContainerGenerator.ContainerFromItem(item) as ComboBoxItem;
itemContainer.Name = "comboAircraftItems" + itemContainer.Content;
}
But note, that the approach you chose has issues. When UI virtualization is enabled for ItemsCopntrol, then only the visible items have their containers generated. Items out side the viewport of the scroll viewer are not generated and can't be returned by the ItemContainerGenerator.
Since UI virtualization is not enabled for the ComboBox by default, this still can work, as long you don't enable this performance optimization feature.
The optimal solution would need you to create a filtered source collection for the ComboBox that displays the filtered items. This way you won't clutter the control with items that are not of interest for the user and are not selectable anyway.
Create a collection of type ObservableCollection<string> which contains the selected items of the primary ComboBox you named comboAircraft.
Set this collection as ItemsSource of the second filtered ComboBox.
Alternatively create a CollectionViewSource of the original source collection and bind it to the secondary ComboBox. ICollectionView supports filtering.
The proper solution
Using either of the two recommended solutions eliminates the requirement to care about item containers. This adds flexibility and simplifies your code.

How to display a list of Objects in a way other than Gridview in Windows Forms

I'm new to C#, having done most of my programming in C and C++.
I need to display a list of objects. I can keep them as an ObservableCollection or BindingList or any list type, but I don't want to bind them to a GridView. My code is below:
public class Item
{
public string Name { get; set; }
public int Price { get; set; }
}
I want to display this class with a label for Name and a button for Price in small groupboxes, and to update them according to updates from the source list. How should I go about approaching this?
You might want to look into the FlowLayoutPanel control. Then you can dynamically build GroupBoxes and add them to the Controls collection. It can be a little difficult to get the hang of building such things dynamically, but it's definitely doable.
This isn't pretty, formatting-wise. But it shows the idea.
FlowLayoutPanel flowLayoutPanel = ...; // define this in the designer
foreach(var v in items)
{
GroupBox gb = new GroupBox();
gb.Text = "Item";
Label label = new Label();
label.Text = v.Name;
gb.Controls.Add(label);
Button btn = new Button();
btn.Text = v.Price.ToString();
gb.Controls.Add(btn);
flowLayoutPanel.Controls.Add(gb);
}

My dynamically created ListViewItems are not displaying in my ListView

I am dynamically creating ListViewItem descendants:
class Application : ListViewItem {
. . .
class LegacyApplication : Application {
I store these first in a List of obects:
private List<object> legacyApps = new List<object>();
..this way:
if (ldbc.appIsLegacy(sPathOfExe)) {
legacyApp = new LegacyApplication(sApp, sTitle, sDesc, sIconFileName, sPathOfExe, appCategoriesForCurrentApp);
}
legacyApps.Add(legacyApp);
...and then I add them to the ListView on the main form this way:
foreach (LegacyApplication LegApp in legacyApps) {
this.listApplications.Items.Add(LegApp);
}
...but the ListView does not display them. It displays the ListView Groups I've created (and each ListViewItem is assigned to one of those groups), but not the ListViewItems themselves...
Updated with requested info:
The constructor for the ListViewItem descendant looks like so:
public LegacyApplication(String AAppName, String ATitle, String ADesc, String AIconFileName, String APathOfExe, List<String> ACategories) {
base.Name = String.Format("legapplvi{0}", AAppName);
base.Text = ATitle; // "Title" is a short description - between exe name and Description
base.ToolTipText = ADesc;
base.EnsureVisible();
// "base" above means ListViewItem; "base" below refers to our Application class*
base.Categories = ACategories;
base.ExePath = APathOfExe;
base.IconFileName = AIconFileName;
}
which adds the following properties to ListViewItem:
public string ExePath
public string IconFileName
public string Category
public List Categories
LegacyApplication adds no further properties to (our) Application class.
I'm not sure what the respondent below means by "subitems" - the ListViewItems are subitems of the ListView Groups, perhaps...?
Updated with unrequested info (TMI?):
OK, I'm thinking I can add columns this way, once all the Groups are assigned to the ListView:
for(var item in listApplications.Groups) {
listApplications.Columns.Add(item)
}
...but now, how do I add specific ListViewItems to particular columns?
Updated after getting it to (sort of) work:
Commenting out this:
listApplications.View = View.Details;
...gets the Items to show. HOWEVER, the Text is truncated, which caused me to pose another question here:
I need to display the entire Text of my ListViewItems (not truncated)
Did you try to create a ListViewItem first and then add into the List instead of using
this.listApplications.Items.Add(LegApp);
Can you provide more details with a code snippet

Synchronize list with listbox?

I have a listbox on my WinForms where users can move the items up and down and that listbox is as well the same as a list I have and I was wondering what would be the most efficient way to maintain both synchronized.
for example to move an item down I have:
int i = this.recoveryList.SelectedIndex;
object o = this.recoveryList.SelectedItem;
if (i < recoveryList.Items.Count - 1)
{
this.recoveryList.Items.RemoveAt(i);
this.recoveryList.Items.Insert(i + 1, o);
this.recoveryList.SelectedIndex = i + 1;
}
And I have:
public List<RouteList> Recovery = new List<RouteList>();
Which I would like to maintain updated against the listbox.
Should I simple clear Recovery and update with the current listbox data or is there a better way to update both when move up and down ?
I am mainly asking because the types from the listbox to the list are different.
.Net provides built-in support for this type of behavior. In order to use it, you need to change the type of your Recovery list to:
public BindingList<RouteList> Recovery = new BindingList<RouteList>();
And then you use that BindingList as the DataSource in your controls:
listBox1.DataSource = Recovery;
Here's a simple example using a BindingList of String. I have two listBox's on the form, and they both stay in sync as the selected element gets swapped with the first element in the list:
public partial class Form1 : Form
{
private readonly BindingList<string> list = new BindingList<string> { "apple", "pear", "grape", "taco", "screwdriver" };
public Form1()
{
InitializeComponent();
listBox1.DataSource = list;
listBox2.DataSource = list;
}
private void listBox1_KeyUp(object sender, KeyEventArgs e)
{
var tmp = list[0];
list[0] = list[listBox1.SelectedIndex];
list[listBox1.SelectedIndex] = tmp;
}
}
The proper way is to change the underlying object and then have the UI Control react to that change.
For the ListBox to react to changes in your object collection (your List) you'd need to use an ObservableCollection instead. It's like the INotifyPropertyChanged for collections.
Then you make your up/down actions change the collection, NOT the UI.
EDIT
I am not saying to add an observer on TOP of the collection. I'm saying to change the type of your collection. Don't use List, use ObservableCollection. It works (largely) the same way but notifies the bound UI Controls of changes to it's items.
As for an example, please Google for it. That's what i'd have to do to provide one anyway..

Binding collections to DataGridView in Windows Forms

I'm trying to bind a collection to a DataGridView. As it turns out it's impossible for the user to edit anything in this DataGridView although EditMode is set to EditOnKeystrokeOrF2.
Here is the simplified code:
public Supplies()
{
InitializeComponent();
List<string> l = new <string>();
l.Add("hello");
this.SuppliesDataGridView.DataSource = l;
}
It also doesn't work when I change the collection type to SortableBindingList, Dictionary or even use a BindingSource.
What can be wrong here?
For me the following method works as expected:
Open your form (usercontrol, etc.) with the designer
Add a BindingSource to your form
Select the BindingSource in your form and open the properties page
Select the DataSource property and click on the down arrow
Click on Add project data source
Select Object
Select the object type you wish to handle
This should be the type that will be handled by your collection, not the CustomCollection itself!
Show the available data sources by selecting from the MenuBar Data - Show Data Sources
Drag and Drop your ItemType from the DatasSources on your form
Go into the code of your form and bind your CustomCollection to the BindingSource
var cc = new CustomCollection();
bindingSource1.DataSource = cc;
Remarks:
The DataGridView is just the last part in your chain to (dis)allow changing, adding and removing objects from your list (or CustomCollection). There is also a property AllowNew within the BindingSource and the ICollection interface has a property IsReadOnly which must be set to false to allow editing. Last but not least, the properties of your class within the collection must have a public setter method to allow changing of a value.
Try this:
public class CustomCollection { public string Value { get; set; } }
public Supplies()
{
InitializeComponent();
List<CustomCollection> l = new List<CustomCollection> { new CustomCollection { Value = "hello" } };
this.SuppliesDataGridView.DataSource = l;
}
Once you've set the DataSource property you'll then want to fire off the DataBind() method.
this.SuppliesDataGridView.DataSource = l;
this.SuppliesDataGridView.DataBind();
UPDATE:
As you rightly pointed out in the comments, the DataBind() method doesn't exist for this control.
This link might provide some helpful information: http://msdn.microsoft.com/en-us/library/fbk67b6z%28v=VS.90%29.aspx

Categories

Resources