Can you data bind a TreeView control? - c#

Typically, when I use the standard TreeView control that comes with C#/VB I write my own methods to transfer data in and out of the Tree's internal hierarchy store.
There might be ways to "bind" the GUI to a data store that I can point to (such as XML files), and when the user edits the tree items, it should save it back into the store. Is there any way to do this?

I got around it by creating a class that Inherits TreeNode and contains an object.
you can then bind a record to the node and recall it during the Click or DoubleClick event.
Eg.
class TreeViewRecord:TreeNode
{
private object DataBoundObject { get; set; }
public TreeViewRecord(string value,object dataBoundObject)
{
if (dataBoundObject != null) DataBoundObject = dataBoundObject;
Text = value;
Name = value;
DataBoundObject = dataBoundObject;
}
public TreeViewRecord()
{
}
public object GetDataboundObject()
{
return DataBoundObject;
}
}
then you can bind to each node as you build your TreeView eg.
TreeView.Nodes.Add(new TreeViewRecord("Node Text", BoundObject));
//or for subNode
TreeView.Nodes[x].Nodes.Add(new TreeViewRecord("Node Text", BoundObject));
Then you can bind the DoubleClick event to something like this
private void TreeViewDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
object exp = ((TreeViewRecord) e.Node).GetDataboundObject();
//Do work
}

Related

Lazy Load Child List Properties in DevExpress Grid (XtraGrid)

I have a simple POCO data structure with what I believe is the correct implementation of loading a list of children in a 'Lazy' manner.
Something like this:
public class Parents
{
public Guid ID { get; set; }
public string Name { get; set; }
//children
private List<Children> childrenList;
public List<Children> ChildrenList
{
get
{
if (childrenList == null) { childrenList = Children.FindByParentID(ID); }
return childrenList;
}
set childrenList = value;
}
}
I need to use a DevExpress XtraGrid control to present the data as it is the control set used by my employer.
I bind the List<Parents> to the grid (e.g. myGrid.DataSource = Parents.FindAll(); ) and everything is fine until I try to expand the first child list using the "+" expansion indicator on the grid.
After I try to expand the first item, the grid (I'm assuming) goes through every parent and tries to load the child list resulting in very slow performance. I can see this while debugging.
I would like the grid to only load the child list for the single item requested. Any ideas about how to achieve this behavior would be very much appreciated.

TreeView item selection vs focus mismatch

So Microsoft has decided to make the process of programmatically selecting items in a TreeView obscenely difficult and malfunctional for some insane reason or other, and the only way to do it (since virtualization ensures that any TreeViewItem you try to select doesn't currently exist) is to create a boolean IsVisible property on whatever you want to use for your source data, which by the way has to implement INotifyPropertyChanged and include an event handler now if it didn't before, and then add an ItemContainerStyle to your TreeView binding the property to the IsVisible property of the TreeViewItem.
What this doesn't do however, is set focus to the selected item, so if for example your goal was to let the user delete tree items with the keyboard and have the focus automatically shift to the deleted item's parent so the user doesn't have to continuously tab through items or click on things to get their place back, this is nearly useless. It somehow doesn't even avoid the virtualization problem, allowing you to set the selection to something that it claims in the next line of code doesn't exist.
Here's my XAML:
and relevant C#:
public partial class MainWindow : Window
{
public ObservableCollection<TreeEdit> Source { get; set; }
public MainWindow()
{
Source = new ObservableCollection<TreeEdit>();
Source.Add(new TreeEdit("Hi"));
DataContext = this;
InitializeComponent();
}
private void KeyNav(object sender, KeyEventArgs e)
{
TreeEdit Selection = (sender as TreeView).SelectedItem as TreeEdit;
ObservableCollection<TreeEdit> TSource = (ObservableCollection<TreeEdit>)(sender as TreeView).ItemsSource;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
if (e.Key == Key.Left && Selection != null)
{
if (Selection.Parent == null)
{
TSource.Remove(Selection);
}
else
{
Selection.Parent.IsSelected = true;
((TreeViewItem)((sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent))).Focus();
Selection.Parent.Remove(Selection);
}
}
}
}
}
public class TagEdit : INotifyPropertyChanged
{
public string Name
{
get
{
return name;
}
set
{
OnPropertyChanged("Name");
name = value;
}
}
private string name;
public bool IsSelected
{
get
{
return selected;
}
set
{
OnPropertyChanged("IsSelected");
selected = value;
}
}
private bool selected;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public TagEdit Parent;
public ObservableCollection<TagEdit> Children { get; set; }
public TagEdit(string n)
{
Name = n;
Children = new ObservableCollection<TagEdit>();
}
public void Remove(TagEdit tag)
{
Children.Remove(tag);
}
}
The idea is that the user can navigate the TreeView normally with the arrow keys, then use Ctrl+Left to delete the selected item and select its parent. (sender as TreeView).ItemContainerGenerator.ContainerFromItem(Selection.Parent) often returns null. Removing it causes the proper item to be selected, but the TreeView loses focus. When it doesn't return null, I get the expected behavior.
Also, despite both the TreeView's KeyboardNavigation.TabNavigation and KeyboardNavigation.ControlTabNavigation being set to the default value of Continue, they behave differently (Tab ignores the TreeView's component elements while Ctrl+Tab steps into them).
I've been trying to get something, anything to work for almost a week now and if my opening paragraph didn't tip you off I've long since worn out my patience. Please don't tell me to try anything unless you have already, personally typed exactly what you're about to tell me to try into VisualStudio and it worked.
Apologies for the harsh tone, but this problem went beyond ridiculous and into obscene some time ago.
The WPF TreeView doesn't have a single ItemContainerGenerator. Every item in a tree view is an ItemsControl and thus has its own ItemContainerGenerator for its child items. What you really need to do is to get the grand parent of the item you are going to delete and use THAT ItemContainerGenerator to call ContainerFromItem.
It's not working because you are trying to use the top level ItemContainerGenerator which only contains the top level items.
P.S. on a friendly side note :), who deletes items with a Ctrl+Left? That's undiscoverable. Why not just do this behavior when they hit Delete?

Can't get ListViewItem when using custom object

I've searched and searched and searched but can't find an answer.
C# and WPF, I have a single ListView with 5 columns and each column has a TextBox in it.
My custom class
public class SomeThing
{
public String field1 { get; set; }
public String field2 { get; set; }
public String field3 { get; set; }
public String field4 { get; set; }
public String field5 { get; set; }
}
My add code
SomeThing item = new SomeThing();
lstItems.Items.Add(item);
My keydown code
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if(e.Key == Key.Return || e.Key == Key.Tab)
{
TextBox tb = (TextBox)sender;
Grid grid = (Grid)tb.Parent;
if (tb.Tag.Equals("Price"))
{
if(lstItems.Items.Count <= lstItems.SelectedIndex + 1) {
SomeThing item = new SomeThing();
lstItems.Items.Add(item);
}
lstItems.SelectedIndex = lstItems.SelectedIndex + 1;
ListViewItem selectedItem = (ListViewItem)lstItems.ItemContainerGenerator.ContainerFromItem(this.lstItems.SelectedItem);
e.Handled = true;
}
}
}
But
ListViewItem selectedItem = (ListViewItem)lstItems.ItemContainerGenerator.ContainerFromItem(this.lstItems.SelectedItem);
Is always null,
this.lstItems.SelectedItem
is just an object of instance "SomeThing".
How do I get the ListView Container?
How do I focus the TextBox on the new selected row?
Please help
It is likely that you are trying to get something from the ItemContainerGenerator that has not been generated yet at the time you ask for it. Adding an item to an ItemsControl (which ListView is a subclass of) does not immediately create a container for that item. There is a delay involved.
This is not really the ideal way to be working with ItemsControl instances. They are really designed around being used with the MVVM design pattern. However, if you need to work with it this way for some reason, then you will need to pay attention to the Status property on the ItemContainerGenerator and the associated StatusChanged event.
So, something along these lines:
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
// You should be able to get the container using ContainerFromItem
}
else
{
// You will have to wait
myItemsControlInstance.ItemContainerGenerator.StatusChanged += myItemsControlInstance_StatusChanged;
}
...
void myItemsControlInstance_StatusChanged(object sender, EventArgs e)
{
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
myItemsControlInstance.ItemContainerGenerator.StatusChanged -= myEventHandler;
// You should be able to get the container now using ContainerFromItem.
// However, layout hasn't been performed on it yet at this point, so there is
// no guarantee that the item is in good condition to be messed with yet.
LayoutUpdated += app_LayoutUpdated;
}
}
void app_LayoutUpdated(object sender, EventArgs e)
{
LayoutUpdated -= app_LayoutUpdated;
if (myItemsControlInstance.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
{
// Now, you can finally get the container using ContainerFromItem and do something with it.
}
else
{
// It looks like more items needed to be generated...
myItemsControlInstance.ItemContainerGenerator.StatusChanged += myItemsControlInstance_StatusChanged;
}
}
There are a few more things you have to watch out for though when working with ItemContainerGenerator directly like this:
The Status on the generator can be Error. You might want to check for that, though I have never seen it happen.
If the ItemsControl is using a virtualizing panel to contain the items, it is possible that you are trying to access an item that is not in view and therefore doesn't exist. You can either override the type of panel used to not virtualize, such as StackPanel instead of VirtualizingStackPanel by setting the ItemsPanel property, or you can make sure the item is scrolled into view somehow before starting the process (which is a whole separate topic).
My recommendation would be to switch to an MVVM model and read up on how to work with ItemsControl in a more natural way, because doing it this way is complicated and error-prone.

fails to overwrite property/class with new empty value before saving to viewstate

i'm building a control that inherits from CompositeControl and creates/maintains a number of dynamically generated child controls. i'm trying to save a class to viewstate before the control tears down the control hierarchy and rebuilds it with a new set of dynamically generated controls. other than persisting this class (as xmlserialized) in viewstate on save, i have no viewstate customization and i am not overriding SaveViewState, SaveControlState, etc.
what's happening in the code below: i have a MySuperClass property on the control that handles my info from load to render. to persist the info through paging events i added a MySuperClassStore to hold onto it. currently, the only data being changed by users is within SubClassControl's SubClass.SubClassResponse (not shown). these changes are being correctly handed back to the SuperClass at the SuperClassControl level (using INotifyPropertyChanged).
the problem: seems to occur on postback (OnControlSave & OnControlPageChange) when you have an existing SuperClass already in viewstate. i'm picking up the existing SuperClass, updating it with new responses, and saving it back to viewstate. but if the SubClassResponse coming from user input is set to empty (by erasing text from or un-selecting the items in the child controls of the SubClassControl), the empty response class (not null; actually initialized as ValueType.Empty) does not update the corresponding entry in the MySuperClassStore before saving back to the viewstate. i've even tried to clear the value in SubClassCollection.UpdateResponse() before setting it, but no luck. i thought i might have a problem with my IEquatable<> but it seems fine. new response values and (non-empty) updated response values are both correctly updated and saved to the viewstate on each save/page change.
BUT (and here's where i'm losing it), when i pop some breakpoints in and step through the code (vs2010), it correctly overwrites the removed entry with the empty (but initialized!) response class and saves back to viewstate (in MySuperClassStore). perfect. every single time. until i quit the debugger, and then it stops working.
stripped down version of my classes:
public class SuperClass {
public Guid SuperClassId { get; set; }
public SubClassCollection ThisCollection { get; set; }
public static SuperClass Deserialize(string s) { /* deserialize xml */ }
}
public class SubClassCollection : KeyedCollection<Guid, SubClass> {
public void UpdateResponse(Guid id, SubClassResponse scr) {
this[id].Response = scr;
//this[id].Response = (!scr.IsEmpty) ? scr : new SubClassResponse();
//** seriously?!? that didn't fix it?
}
}
public class SubClass {
public Guid SubClassId { get; set; }
public int SomeIntProp { get; set; }
private SubClassResponse _response;
public SubClassResponse Response {
get { return (_response != null) ? this._response : new SubClassResponse(); }
set { if (!_response.Equals(value)) { _response = value; OnPropertyChanged("Response"); } }
}
}
public class SubClassResponse : IEquatable<SubClassResponse> {
public string Value { get; set; }
public ValueType ThisValueType { get; set; }
public bool IsEmpty {
switch (this.ThisValueType) {
case ValueType.Empty: return true; break;
case ValueType.StringValue: return String.IsNullOrEmpty(this.Value); break;
case ValueType.ListItemCollection: /* check for null, then .Count > 0 */ break;
default: return true; break;
}
}
public enum ValueType { Empty, StringValue, ListItemCollection, Etc }
}
and my top-level control has this going on:
public class SuperClassControl : CompositeControl, INamingContainer {
// OnInit: load MySuperClass from db
// OnPreRender: get MySuperClassStore from viewstate and output as debug info
protected SuperClass MySuperClass { get; set; }
protected SuperClass MySuperClassStore {
get {
return (ViewState["SuperClassStore"] == null) ? new SuperClass() : SuperClass.Deserialize((string)ViewState["SuperClassStore"]);
}
set { ViewState["SuperClassStore"] = value.ToSerialized(); }
}
protected override void CreateChildControls() {
// generate control hierarchy
SuperClass mysuperclass = this.MySuperClass;
foreach (SubClass mysubclass in mysuperclass.ThisCollection) {
this.Controls.Add(new SubClassControl(mysubclass));
}
// add top-level control linkbuttons, hook up events, etc...
}
protected void OnControlSave(object sender, EventArgs e) {
SuperClass mysuperclass = this.MySuperClass;
SuperClass mystorageclass = this.MySuperClassStore;
// loop through control hierarchy, make sure we've got any response changes
// copy the changes to mystorageclass for persistence
// ***** problem is occurring here?? *****
foreach (Guid g in mysuperclass.ThisCollection.HasChanges) {
mystorageclass.ThisCollection.UpdateResponse(g, mysuperclass.ThisCollection[g].Response);
}
// if the current page only displayed i=5 to 8, will only update i=5 to 8
this.MySuperClassStore = mystorageclass;
}
protected void OnControlPageChange(object sender, EventArgs e) {
OnControlSave(sender, e);
// set next or previous page index then reset control hierarchy
}
}
any thoughts? i've been messing with this for several days now, and i'm running out of ideas. thanks in advance!
will
so, i solved this problem by inverting the update process:
mystorageclass.ThisCollection.UpdateResponse(g, mysuperclass.ThisCollection[g].Response);
this.MySuperClassStore = mystorageclass;
to:
mysuperclass.ThisCollection.UpdateResponse(g, mystorageclass.ThisCollection[g].Response);
this.MySuperClassStore = mysuperclass;
where the loop gets all SubClass NOT existing on the current page.
i works, so "yay?" - but i'm still at a loss for why this actually works better than the original piece of code, so if anyone has some insight into this, i'll happily give you points for the answer!

Custom detail pages in Windows 8 grid application

I have created a simple C# Windows 8 grid application.
If you're unfamiliar with this layout, there is a brief explanation of it here :
Link
What I would like to have is simple - some custom ItemDetailPages. I'd like to be able to click on some items on the GroupDetailPage and the GroupedItemsPage and navigate to a custom .xaml file, one where I can include more than one image.
I'm sure there is a simple way of doing that that I have missed out on, and I'm also sure that this information will be useful for a lot of people, so I will be offering a bounty on this question.
I have struggled with doing this so far :
I've created a CustomDataItem in the SampleDataSource.cs class :
/// <summary>
/// Generic item data model.
/// </summary>
public class CustomDataItem : SampleDataCommon
{
public CustomDataItem(String uniqueId, String title, String subtitle, String imagePath, String description, String content, SampleDataGroup group)
: base(uniqueId, title, subtitle, imagePath, description)
{
this._content = content;
this._group = group;
}
private string _content = string.Empty;
public string Content
{
get { return this._content; }
set { this.SetProperty(ref this._content, value); }
}
private SampleDataGroup _group;
public SampleDataGroup Group
{
get { return this._group; }
set { this.SetProperty(ref this._group, value); }
}
}
However, obviously, adding to the ObservableCollection
private ObservableCollection<SampleDataGroup> _allGroups = new ObservableCollection<SampleDataGroup>();
public ObservableCollection<SampleDataGroup> AllGroups
{
get { return this._allGroups; }
}
is impossible with a different data type. So what can I do in this case ?
Thanks a lot.
I have a simple grid application; how do I make it possible to have one of the elements in the group item page link to a custom item detail page ?
Ok, lets take the app that is generated when using the "Grid App" template from Visual Studio.
The data class for the elements on the group items page is the SampleDataItem class. What you can do is add some type of data field (bool, int, or other) that indicates how to handle the navigation. In this example, we are keeping it simple, so we add a bool to indicate whether the navigation is custom or not.
public class SampleDataItem : SampleDataCommon
{
// add flag as last param
public SampleDataItem(String uniqueId, String title, String subtitle,
String imagePath, String description, String content, SampleDataGroup group,
bool isCustomNav = false)
: base(uniqueId, title, subtitle, imagePath, description)
{
this._content = content;
this._group = group;
this.IsCustomNav = isCustomNav;
}
// to keep it simple this doesn't handle INotifyPropertyChange,
// as does the rest of the properties in this class.
public bool IsCustomNav { get; set; }
...
}
So when you are adding a new SampleDataItem object to be displayed, you just need to set the isCustomNav field in the constructor.
Now all we have to do is change the already existing click event handler in the grid on the grouped item page (GroupedItemsPage.xaml.cs):
void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
// Navigate to the appropriate destination page, configuring the new page
// by passing required information as a navigation parameter
var item = (SampleDataItem)e.ClickedItem;
var itemId = item.UniqueId;
if (item.IsCustomNav == false)
{
// default
this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}
else
{
// custom page
this.Frame.Navigate(typeof(ItemDetailPage2), itemId);
}
}
All we are doing above is getting the selected item and then testing the navigation flag that we added earlier. Based on this we navigate to either the original ItemDetailPage or a new one called ItemDetailPage2. As I mentioned before, the navigation flag doesn't have to be a bool. It can be an int or enum or some other type that tells us where to navigate.
Note that if you want similar behavior on the GroupDetailsPage, you just have to update the click event handler there the same way.
Hope that helps.
Yes you should be able to create a custom or different data type. If you create a Win8 app using the grid template, you see that the template does three things for you:
1) It creates three types, SampleDataCommon, which is the base, SampleDataItem, which implements SampleDataCommon and adds two new properties - content and group, and SampleDataGroup which also implements SampleDataCommon, adds a method, ItemsCollectionChanged, and adds two properties, Items and TopItems.
2) It creates a class called SampleDataSource, in which a collection of SampleDataGroup is created and named AllGroups: ObservableCollection AllGroups.
3) It binds Items and AllGroups of SampleDataSource to objects in XMAL pages.
In your case, you use the same data structure. In other words, you will create a group with items, etc.

Categories

Resources