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?
Related
I'm binding the title of my Xamarin.Forms.ContentPage to a property BuggyTitle in my view model (VM). The VM derives from MvxViewModel. Here's the simplified version:
BuggyPage.xaml:
<?xml version="1.0" encoding="UTF-8"?>
<local:ContentPage Title="{Binding BuggyTitle}"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyProject.BuggyPage"
xmlns:local="clr-namespace:Xamarin.Forms;assembly=MyProject">
<ContentPage.Content NavigationPage.HasNavigationBar="false">
<Grid>
<ScrollView>
<!--and so on-->
</ContentPage.Content>
</local:ContentPage>
BuggyViewModel.cs:
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
public string BuggyTitle {get; set;}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
RaisePropertyChanged("BuggyTitle"); // this seems to make no difference
}
}
}
There's not much going on in the code behind other than a call to InitializeComponent() in the constructor.
The page is mapped to the VM generically in my project (not actually 'my' project, it's existing design), and it boils down to these (again, simplified) lines of code:
public static Page CreatePage(MvxViewModelRequest request)
{
var viewModelName = request.ViewModelType.Name;
var pageName = viewModelName.Replace ("ViewModel", "Page");
var pageType = (typeof (MvxPagePresentationHelpers)).GetTypeInfo ().Assembly.CreatableTypes().FirstOrDefault(t => t.Name == pageName);
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
var viewModel = viewModelLoader.LoadViewModel(request, null);
var page = Activator.CreateInstance(pageType) as Page;
page.BindingContext = viewModel;
return page;
}
The problem:
When BuggyPage loads, I initially get the correct value for the title. Whenever it is displayed after that, even though I can see in the debugger that BuggyTitle is getting updated correctly, the change does not appear in the page.
Question:
Why don't updates to BuggyTitle get reflected in the page?
Edit 1:
To further describe the weirdness, I added a Label to my ContentPage, with x:Name="BuggyLabel" and Text="{Binding BuggyLabelText}".
In my code-behind, I added this:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
BuggyLabel.Text = binding_context.BuggyLabelText;
}
I set a breakpoint at BuggyLabel.Text =. It gets hit every time the page loads, and BuggyLabel.Text already seems to have the correct value (i.e, whatever binding_context.BuggyLabelText is set to). However, the actual page displayed only ever shows what the text in this label is initially set to.
And yes, have clean/built about a million times.
Edit 2 (further weirdness):
I put this in the code-behind so that it runs during page load:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
binding_context.RefreshTitleCommand.Execute(null);
});
}
This again changes values in the debugger, but these changes don't get reflected in the displayed page.
I then added a button to the page and bound it to RefreshTitleCommand, and wham! the page updates its display.
Unfortunately I can't use this. Not only is it incredibly hackish, I can't have the user pressing buttons to have the page display what it's meant to on load.
I wonder if there's some caching going on with MvvmCross or Xamarin.
Answer
You need to add RaisePropertyChanged in BuggyTitle property declaration.
ViewModel
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() => BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
}
}
-----New Update------
Code behind code
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
BuggyLabel.Text = binding_context.BuggyLabelText;
});
}
I don't have any experience at all with Xamarin (but i do want to try it out in the future when i get as comfortable as possible with UWP), but i guess the Data Binding process should be working similar to what i am used to there ...
You are mentioning that you have no problem with the values that are set when the page first loads, however when you actually update the values there's no "linking" to the visual layer, despite at debug time you actually seeing the value being set to something completely different from it's initial state.
Since you are dealing with properties-only viewmodel (Collections for instance in UWP are another level of events which need to be exposed), RaisePropertyChanged seems like the correct choice.
What i cannot understand is if when you first create your page, the Binding which you are creating is at least specified as One-Way mode, so changes in your viewmodel properties are propagated onto your UI when their set accessor methods are called.
You are setting your page context to viewmodel (each i figure is the same as DataContext in UWP/WPF), and therefore you can actually access those properties with the {Binding } markup. But what is the default mode for this operation in Xamarin ? (in UWP it is actually OneWay, and therefore it would work right of the bat for this situation ...).
I have seen that in Xamarin it might be a bit different , since you also have the Default option. Can that be it?
PS. Hopefully this might be useful to you, despite my lack of experience with Xamarin.
Edit2
Implementing the INotifyPropertyChanged,
public class BuggyViewModel : MvxViewModel, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() =>
BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I was setting a controls binding context from the property changed event of one of its properties.
This made my control stop tracking changes despite everything else binding correctly still, and the control would also correctly bind initially (first time is fine, further changes do not fire the property changed event again).
page.BindingContext = viewModel;
Ok let's say we have a Parent Window bound to a DataContext now assume we fire a Child Window and set its data context to that of the parent window. Now the items in the child window are bound to the data context in TwoWay mode. When the user changes something in this child window the data in the source viewmodel will be updated. But what if the user closes the window with DialogResult == false? How to rollback the data to its original?
For now I'm binding the data in OneWay mode and applying the changes only when the DialogResult == true. But I guess there must be a more elegant way to do this.
IEditableObject
In the same way that WPF can use System.ComponentModel.INotifyPropertyChanged you can also use the interface System.ComponentModel.IEditableObject. This interface has the following useful methods:
BeginEdit()
EndEdit() - not really required for our use
CancelEdit()
Prior to displaying your window, call BeginEdit() on your datacontext. Now any changes reflect via the usual INotifyPropertyChanged so that the parent receives updates as usual.
If the user cancels, then call CancelEdit() at which point your viewmodel should rollback the changes. The free set of steak knives is that your parent window is informed of the rollback via INotifyPropertyChanged thus "reverting" the parent window too.
Otherwise call EndEdit() which honestly isn't required as the changes are done already.
Obviously you would need to remember somehow the prior values so that you can revert any changes.
IRevertibleChangeTracking
I've just read about System.ComponentModel.IRevertibleChangeTracking which offers the methods AcceptChanges() and RejectChanges(). Though arguably this is the interface to use for accepting and rolling back changes, it's not entirely clear whether changes made in the interim should be broadcast in the WPF scenario. Perhaps someone can help me here.
Tell me more...
Change the UpdateSourceTrigger on the binding to be explicit and call the UpdateSource method only when the OK button is clicked.
UpdateSourceTrigger
For example, you can bind the confirmed values as one-way, readonly in the main view and the uncommitted ones to the dialog text boxes.
ViewModel
class MyVM : MVVM.ViewModel.ViewModelBase
{
private string name1;
public string Name1
{
get { return name1; }
set {
name1 = value;
OnPropertyChanged(() => Name1);
}
}
string name1Conf;
public string Name1Conf
{
get { return name1Conf; }
}
private string name2;
public string Name2
{
get { return name2; }
set
{
name2 = value;
OnPropertyChanged(() => Name2);
}
}
string name2Conf;
public string Name2Conf
{
get { return name2Conf; }
}
private bool commitMe;
public bool CommitMe
{
get { return commitMe; }
set {
commitMe = value;
OnPropertyChanged(() => CommitMe);
if (commitMe)
{
DoCommit();
}
}
}
private void DoCommit()
{
name1Conf = name1;
name2Conf = name2;
OnPropertyChanged(() => Name1Conf);
OnPropertyChanged(() => Name2Conf);
}
}
CodeBehind
private void button_Click(object sender, RoutedEventArgs e)
{
var win2 = new WPFDialog();
win2.DataContext = myVM;
var res = win2.ShowDialog();
myVM.CommitMe = res == true;
}
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.
I am binding a WPF application DataGrid to an ObservableCollection via the DataGrid's "ItemSource". Initially the DataGrid does come up with headings and values, however the upgrades made to the ObservableCollection are not reflected? (i.e. when I come back programmatically and increase the "Total" value) The ObservableCollection I am using is below.
Any ideas why & how to get the grid to dynamically update/bind correctly?
public class SummaryItem
{
public string ProcessName { get; set; }
public long Total { get; set; }
public long Average { get; set; }
public static SummaryItem ObservableCollectionSearch(ObservableCollection<SummaryItem> oc, string procName)
{
foreach (var summaryItem in oc)
{
if (summaryItem.ProcessName == procName) return summaryItem;
}
return null;
}
}
EDIT - Or perhaps an add-on question is whether in this case DataGrid isn't the control I should be using to visualize what is effectively an in-memory table? That is the observableCollection of SummaryItem's is effectively the in-memory table.
If I see it right you are using an ObservableCollection. If you add items to the ObservableCollection these changes should always been reflected by WPF, but if you edit properties on an item (i.e. changing the "Total" value of a SummaryItem) this is no change to the ObservableCollection but to the SummaryItem.
To achieve the desired behaviour your SummaryItems have to implement the INotifyPropertyChanged interface to "notify" WPF when propertys are changed:
// implement the interface
public event PropertyChangedEventHandler PropertyChanged;
// use this for every property
private long _Total;
public long Total {
get {
return _Total;
}
set {
_Total = value;
if(PropertyChanged != null) {
// notifies wpf about the property change
PropertyChanged(this, new PropertyChangedEventArgs("Total"));
}
}
}
you have just ran into the classic problem with ObservableCollection. Only item add and item remove events are fired for OC. That means, if an item changes, you do NOT get an "ItemChanged" event.
ObservableCollection only raise event when you add or remove items, if you need to raise event even if any item inside the collection changes use BindingList.
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
}