I have several custom user controls in a window. They appear dynamically, like workspaces.
I need to add a dependency property on an itemscontrol to trigger a scrolldown when an item is being added to the bound observable collection to my itemscontrol, like so:
(usercontrol)
<ScrollViewer VerticalScrollBarVisibility="Auto" >
<ItemsControl Grid.Row="0" ItemsSource="{Binding Messages}" View:ItemsControlBehavior.ScrollOnNewItem="True">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox IsReadOnly="True" TextWrapping="Wrap" Text="{Binding Path=DataContext, RelativeSource={RelativeSource Self}}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
And the code of my dependency property :
public class ItemsControlBehavior
{
static readonly Dictionary<ItemsControl, Capture> Associations =
new Dictionary<ItemsControl, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ItemsControl),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var mycontrol = d as ItemsControl;
if (mycontrol == null) return;
bool newValue = (bool)e.NewValue;
if (newValue)
{
mycontrol.Loaded += new RoutedEventHandler(MyControl_Loaded);
mycontrol.Unloaded += new RoutedEventHandler(MyControl_Unloaded);
}
else
{
mycontrol.Loaded -= MyControl_Loaded;
mycontrol.Unloaded -= MyControl_Unloaded;
if (Associations.ContainsKey(mycontrol))
Associations[mycontrol].Dispose();
}
}
static void MyControl_Unloaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
Associations[mycontrol].Dispose();
mycontrol.Unloaded -= MyControl_Unloaded;
}
static void MyControl_Loaded(object sender, RoutedEventArgs e)
{
var mycontrol = (ItemsControl)sender;
var incc = mycontrol.Items as INotifyCollectionChanged;
if (incc == null) return;
mycontrol.Loaded -= MyControl_Loaded;
Associations[mycontrol] = new Capture(mycontrol);
}
class Capture : IDisposable
{
public ItemsControl mycontrol{ get; set; }
public INotifyCollectionChanged incc { get; set; }
public Capture(ItemsControl mycontrol)
{
this.mycontrol = mycontrol;
incc = mycontrol.ItemsSource as INotifyCollectionChanged;
incc.CollectionChanged +=incc_CollectionChanged;
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
ScrollViewer sv = mycontrol.Parent as ScrollViewer;
sv.ScrollToBottom();
}
}
public void Dispose()
{
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
During the first instantiation of my user control, it works like a charm.
But when another user control of the same type is dynamically instantiated, the DependencyProperty is never attached anymore to my scrollviewer. Only the first instance will work correctly.
I know that dependency properties are static, but does that mean they can't work at the same time on several user control of the same type added to the window?
Update 02/03 : Here's how I set the viewmodel to the view (not programmatically) :
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:testDp.ViewModel"
xmlns:View="clr-namespace:testDp.View">
<DataTemplate DataType="{x:Type vm:ChatTabViewModel}">
<View:ChatTabView />
</DataTemplate>
</ResourceDictionary>
even with x:shared = false in the datatemplate tag, it won't work.
But if I set the datacontext in a classic way like usercontrol.datacontext = new viewmodel(), it definitely work. But it's recommended to have a "shared" view, so how do we make dependency properties work with this "xaml" way of setting datacontext ?
Sorry, I couldn't reproduce your problem.
I started Visual C# 2010 Express, created a new 'WPF Application', added your XAML to a UserControl that I imaginatively titled UserControl1, and added your ItemsControlBehavior class. I then modified the MainWindow that VC# created for me as follows:
MainWindow.xaml (contents of <Window> element only):
<StackPanel Orientation="Vertical">
<Button Content="Add user control" Click="ButtonAddUserControl_Click" />
<Button Content="Add message" Click="ButtonAddMessage_Click" />
<StackPanel Orientation="Horizontal" x:Name="sp" Height="300" />
</StackPanel>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public ObservableCollection<string> Messages { get; private set; }
public MainWindow()
{
InitializeComponent();
Messages = new ObservableCollection<string>() { "1", "2", "3", "4" };
DataContext = this;
}
private void ButtonAddUserControl_Click(object sender, RoutedEventArgs e)
{
sp.Children.Add(new UserControl1());
}
private void ButtonAddMessage_Click(object sender, RoutedEventArgs e)
{
Messages.Add((Messages.Count + 1).ToString());
}
}
I made no modifications to the XAML in your UserControl, nor to your ItemsControlBehavior class.
I found that no matter how many user controls were added, their ScrollViewers all scrolled down to the bottom when I clicked the 'Add message' button.
If you're only seeing the scroll-to-the-bottom behaviour on one of your user controls, then there must be something that you're not telling us.
Related
I have an application in which I set the content of a contentpresenter, dependent on the datatype by a datatemplate (see MainWindow). The Datatemplate is a usercontrol, which is actually datatype specific. (The small example below is only for demonstration, but in my "real" application the user shall be able to switch between different data.)
The usercontrol (UserControl1) has a DependencyProperty which I assign a value (in my application this is actually a binding to a VM, just set it to a string in example for simplicity).
Setting the value is still working fine. However In my UserControl I need to react to changes of the DependencyProperty to change the view of my UserControl (or later on CustomControl). So I implemented a OnPropertyChangend method.
When application starts OnPropertyChanged works as I expect it and I get the "correct" newvalue of my DependencyProperty. However, if I change my VM (i.e. my datatemplate changes) during runtime by clicking on a button, OnPropertyChanged returns the DependencyProperty's defaultvalue.
In my small example application, I can see that the value is set correctly, as the Textblock content changes to the correct value.
It only seems that OnPropertyChanged gets fired before my DependencyProperty's value gets the new value. So, it's not possible for me to react on the new value.
It is not really clear why this happens. Seems to have something to do with the order in which WPF resolves internal stuff?
Does anyone have a clue, how I can fix this behavior and get access to the current/last value when changing my VM and don't miss an update? As stated out before, I need to react on that value.
Maybe I am doing something totally stupid here. Is the approach I decided to use here a bad one? Are DataTemplates the wrong approach to switch between two pairs? What would be a better approach then? However, I guess it won't be possible to avoid the DependencyProperty and the UserControl in my application.
MainWindow.xaml
<!--MainWindow.xaml -->
<Grid>
<StackPanel>
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ContentPresenter Content="{Binding ActiveVM}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:VM1}">
<local:UserControl1 MyProperty="Test1"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:VM2}">
<local:UserControl1 MyProperty="Test2"/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
</Grid>
MainWindow.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
vmParent = new VMParent();
DataContext = vmParent;
var vm1 = new VM1();
var vm2 = new VM2();
}
VMParent vmParent;
private void Button_Click(object sender, RoutedEventArgs e)
{
vmParent.ChangeActiveVM();
}
}
UserControl1.xaml
<!--UserControl1.xaml -->
<TextBlock Text="{Binding MyProperty, RelativeSource={RelativeSource AncestorType={x:Type local:UserControl1}}}"/>
UserControl1.cs
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public string MyProperty
{
get { return (string)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(UserControl1), new PropertyMetadata("DefaultString", OnMyPropertyChangend));
private static void OnMyPropertyChangend(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == "DefaultString")
{
;
//xxxxxx
//unexpectedly i get stuck here
//Would expect/need NewValue to be Text1/Text2 to react to it
//xxxxxx
}
}
}
VMParent
class VMParent : INotifyPropertyChanged
{
public VMParent()
{
vm1 = new VM1();
vm2 = new VM2();
ActiveVM = vm1;
}
public event PropertyChangedEventHandler PropertyChanged;
VM1 vm1;
VM2 vm2;
public object ActiveVM
{
get => m_activeVM;
set { m_activeVM = value; OnPropertyChanged("ActiveVM"); }
}
private object m_activeVM;
protected internal void OnPropertyChanged(string propertyname)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
}
public void ChangeActiveVM()
{
if (ActiveVM is VM1)
ActiveVM = vm2;
else
ActiveVM = vm1;
}
}
VMs are only used to apply Datatemplate
class VM1
{
}
class VM2
{
}
I'm developing an autocomplete user control for WPF using XAML and C#.
I would like to have the pop-up for the suggestions to appear above all controls. Currently my pop up is a ListView . That causes problems since whenever I decide to show it the UI must find a place for it and to do so moves all the controls which are below it further down.
How can I avoid this? I assume I must put it in a layer which is above all of the other controls?
I have written "auto-complete" style controls before by using the WPF Popup control, combined with a textbox. If you use Popup it should appear, as you say, in a layer over the top of everything else. Just use Placement of Bottom to align it to the bottom of the textbox.
Here is an example that I wrote a while ago. Basically it is a text box which, as you type pops up a suggestions popup, and as you type more it refines the options down. You could fairly easily change it to support multi-word auto-complete style code editing situations if you wanted that:
XAML:
<Grid>
<TextBox x:Name="textBox"
Text="{Binding Text, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:IntelliSenseUserControl}}}"
KeyUp="textBox_KeyUp"/>
<Popup x:Name="popup"
Placement="Bottom"
PlacementTarget="{Binding ElementName=textBox}"
IsOpen="False"
Width="200"
Height="300">
<ListView x:Name="listView"
ItemsSource="{Binding FilteredItemsSource, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:IntelliSenseUserControl}}}"
SelectionChanged="ListView_Selected"/>
</Popup>
</Grid>
Code-behind:
public partial class IntelliSenseUserControl : UserControl, INotifyPropertyChanged
{
public IntelliSenseUserControl()
{
InitializeComponent();
DependencyPropertyDescriptor prop = DependencyPropertyDescriptor.FromProperty(ItemsSourceProperty, typeof(IntelliSenseUserControl));
prop.AddValueChanged(this, ItemsSourceChanged);
}
private void ItemsSourceChanged(object sender, EventArgs e)
{
FilteredItemsSource = new ListCollectionView((IList)ItemsSource);
FilteredItemsSource.Filter = (arg) => { return arg == null || string.IsNullOrEmpty(textBox.Text) || arg.ToString().Contains(textBox.Text.Trim()); };
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(IntelliSenseUserControl), new FrameworkPropertyMetadata(null) { BindsTwoWayByDefault = true });
public object ItemsSource
{
get { return (object)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(object), typeof(IntelliSenseUserControl), new PropertyMetadata(null));
#region Notified Property - FilteredItemsSource (ListCollectionView)
public ListCollectionView FilteredItemsSource
{
get { return filteredItemsSource; }
set { filteredItemsSource = value; RaisePropertyChanged("FilteredItemsSource"); }
}
private ListCollectionView filteredItemsSource;
#endregion
private void textBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Return || e.Key == Key.Enter)
{
popup.IsOpen = false;
}
else
{
popup.IsOpen = true;
FilteredItemsSource.Refresh();
}
}
private void UserControl_LostFocus(object sender, RoutedEventArgs e)
{
popup.IsOpen = false;
}
private void ListView_Selected(object sender, RoutedEventArgs e)
{
if (listView.SelectedItem != null)
{
Text = listView.SelectedItem.ToString().Trim();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
If your Window's content container is a Grid, you can simply do something like
<ListBox Grid.RowSpawn="99" Grid.ColumnSpan="99"/>
to "simulate" an absolute position. You then just have to set its position with Margin, HorizontalAlignment and VerticalAlignment so it lays around the desired control.
I am having no luck trying to bind a collection of data to my custom control its property. I already have implemented the mechanism for a string property of this control (with a small help from here) and expected the collection type to be as easy. However I cannot make it work again.
Here is my custom control view
<UserControl x:Class="BadaniaOperacyjne.Controls.Matrix"
mc:Ignorable="d" Name="CustomMatrix"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<!-- ... -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<!-- ... -->
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ElementName=CustomMatrix, Path=Title}"/>
<Grid Grid.Row="2" Grid.Column="1" Name="contentGrid">
<ListBox ItemsSource="{Binding ElementName=CustomMatrix, Path=ItemsList}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</UserControl>
and its code-behind
#region ItemsList Property
public static readonly DependencyProperty ItemsListProperty =
DependencyProperty.Register("ItemsList", typeof(ObservableCollection<object>), typeof(Matrix), new PropertyMetadata(new ObservableCollection<object>(), new PropertyChangedCallback(ItemsListChanged)));
public ObservableCollection<object> ItemsList
{
get { return GetValue(ItemsListProperty) as ObservableCollection<object>; }
set { SetValue(ItemsListProperty, value); }
}
private void ItemsListChanged(object value)
{
System.Diagnostics.Debug.WriteLine("matrix: items list changed " + value);
if (ItemsList != null)
{
ItemsList.CollectionChanged += ItemsList_CollectionChanged;
System.Diagnostics.Debug.WriteLine("got " + string.Join(",", ItemsList.ToList()));
}
else
{
System.Diagnostics.Debug.WriteLine("got null");
}
}
void ItemsList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("matrix: current items list collection changed");
}
private static void ItemsListChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// a breakpoint
((Matrix)d).ItemsListChanged(e.NewValue);
}
#endregion
// showing the Title property implementation just to state that
// it is done the same way as for ItemsList
#region Title Property
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(Matrix), new PropertyMetadata("", new PropertyChangedCallback(TitleChanged)));
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
private void TitleChanged(string title)
{
System.Diagnostics.Debug.WriteLine("matrix: title changed to: " + title);
}
private static void TitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((Matrix)d).TitleChanged((string)e.NewValue);
}
#endregion
And here's how I am trying to bind to that control
<custom:Matrix x:Name="customMatrix" DockPanel.Dock="Top" Title="{Binding Title}" ItemsList="{Binding Items}"/>
and the code-behind for the main page is
//internal ObservableCollection<List<int>> ItemsList { get; set; }
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel()
{
Items = new ObservableCollection<int> { 1, 2, 3, 4, 5, 6};
}
void Items_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine("problem manager: items list changed " + e.NewItems.Count);
}
public ObservableCollection<int> Items { get; set; }
protected string title;
public string Title
{
get { return title; }
set
{
if (title != value)
{
title = value;
NotifyPropertyChanged("Title");
}
}
}
protected void NotifyPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
public ViewModel VM { get; private set; }
// this is the window constructor
private ProblemManager()
{
VM = new ViewModel();
DataContext = VM;
InitializeComponent();
VM.Title = "title";
}
private int i = 0;
private void btnAddRow_Click(object sender, RoutedEventArgs e)
{
// when doing either of these two lines below,
// the control breakpoint is never hit
VM.Items.Add(++i);
VM.Items = new ObservableCollection<int> { 2, 3 };
// however, when directly assigning to the control's property,
// the event is raised and the breakpoint is hit and the UI is updated
customMatrix.ItemsList = new ObservableCollection<object> { 1, 2, 3 };
customMatrix.ItemsList.Add(66);
// and this of course makes the control's title update
VM.Title = (++i).ToString();
}
Both DependencyPropertys for the control's Title and ItemsList are, I believe, created the same way. Nonetheless, the binding is probably not working as the ItemsListChanged event is not raised by that binding.
So, the problem is that I cannot bind my window's ViewModel.Items collection via XAML to the control's ItemsList collection. Is creating a DependencyProperty for a collection within a control any different from DependencyProperty for a simple string property?
Problem is in your DependencyProperty registration. Co-variance is not applicable for generic lists i.e. you cannot do this -
ObservableCollection<object> objects = new ObservableCollection<int>();
You have declared type of DP as ObservableCollection<object> but binding it with list of type ObservableCollection<int>.
You should change either type of DP to ObservableCollection<int> OR change binding collection type to ObservableCollection<object>.
public ViewModel()
{
Items = new ObservableCollection<object> { 1, 2, 3, 4, 5, 6};
}
public ObservableCollection<object> Items { get; set; }
After such a long time, I kind of needed this to work in both ways:
Be able to define a collection of any kind of items
Be informed when an item was added/removed and so on.
I am working on a bread crumb like control to be more precise.
Indeed, the ItemsSource DP is defined with an IList type, to allow the "genericity" aforementioned:
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IList), typeof(BreadCrumbUserControl),
new PropertyMetadata(null, new PropertyChangedCallback(OnItemsSourceChanged)));
And here is how I use my BreadCrumb:
<views:BreadCrumbUserControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"/>
where Items is an ObservableCollection of a custom type:
public ObservableCollection<BaseItem> Items { get; set; }
Indeed it works fine, my ItemsSource local property points to the right collection but I need to be informed in my UserControl when an item is added to the ObservableCollection.
So, I've made use of the PropertyChangedCallback delegate on my ItemsSource DP.
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
BreadCrumbUserControl thisUserControl = (BreadCrumbUserControl)d;
(e.NewValue as INotifyCollectionChanged).CollectionChanged += thisUserControl.BreadCrumbUserControl_CollectionChanged;
}
This is the trick that worked for me, because my input reference is in fact the ObservableCollection defined in the upper layers.
Both goals are now achieved: work with collection of any custom types and in the same type, stay informed.
If a protocol is set, reflection can be used to change values on our custom type.
Here is just an example of iterating on my collection which has some unknown type in my UserControl's world:
foreach (var baseItem in ItemsSource)
{
baseItem.GetType().GetProperty("IsActive").SetValue(baseItem, false);
}
The feature I'm working on is autocomplete for keyword search. As soon as the user inputs something into search bar, the view model calls the autocomplete api with keyword parameter to get the autocomplete suggestions back and put them into a observablecollection container. This observablecollection is a dependency property, it's bound with the list box to show the autocomplete suggestions. My problem is the dependency property is populated correctly but the list box doesn't display anything. Following are some code pieces:
data binding in xaml.cs:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
searchBar.Focus();
_searchViewModel = new SearchViewModel();
DataContext = _searchViewModel;
}
invoke a method in view model to call the autocomplete api:
private void searchBar_TextChanged(object sender, TextChangedEventArgs e)
{
_searchViewModel.getTypeaheadListFromServer(searchBar.Text);
}
dependency property in view model, it's populated successfully:
public ObservableCollection<TypeaheadElement> TypeaheadList
{
get { return (ObservableCollection<TypeaheadElement>)GetValue(TypeaheadListProperty); }
set { SetValue(TypeaheadListProperty, value); }
}
// Using a DependencyProperty as the backing store for TypeaheadList. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TypeaheadListProperty =
DependencyProperty.Register("TypeaheadList", typeof(ObservableCollection<TypeaheadElement>), typeof(SearchViewModel), new PropertyMetadata(null));
data binding in xaml:
<ListBox Name="typeahead" Grid.Row="1" ItemsSource="{Binding TypeaheadList}" Height="518" Margin="0,0,0,-518" SelectionChanged="typeahead_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock TextWrapping="Wrap" Text="{Binding TypeaheadElementStr}" FontSize="{StaticResource ListItemFontSize}" FontFamily="Segoe WP" Margin="10,0,0,0" VerticalAlignment="Top">
<TextBlock.Foreground>
<SolidColorBrush Color="{StaticResource ListItemFontColor}"/>
</TextBlock.Foreground>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Thank you very much for your help!
try this
<ListBox Name="typeahead" Grid.Row="1" ItemsSource="{Binding TypeaheadList, UpdateSourceTrigger=PropertyChanged}" Height="518" Margin="0,0,0,-518" SelectionChanged="typeahead_SelectionChanged">
I don't understand why you try to implement DependencyProperty is this situations. TypeaheadList is a source of Binding, not a target, right? So it can be a simple property on your ViewModel.
Have you tried using the AutoCompleteBox from the toolkit? If the list of possibilities are not large, you can pre-populate the ItemsSource of the AutoCompleteBox. If you cannot pre-populate it, you could issue an async request to the server to get all of the possibilities when the app starts.
Here are some blogs about using the AutoCompleteBox:
http://www.jeff.wilcox.name/2011/03/acb-in-pivot/
If that is not possible, then you can do something like the following:
Xaml:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<toolkit:AutoCompleteBox ItemsSource="{Binding People}" Populating="AutoCompleteBox_Populating" />
</Grid>
Code:
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
People = new ObservableCollection<string> {"Shawn", "steve", "Bob", "randy", "mike"};
DataContext = this;
InitializeComponent();
}
public ObservableCollection<string> People { get; set; }
private void AutoCompleteBox_Populating(object sender, PopulatingEventArgs e)
{
// Have we already populated with this text?
if(People.Any(person => person.ToLower().StartsWith(e.Parameter.ToLower()))) return;
Completer c = new Completer();
c.Completed += new EventHandler<EventArgs>(c_Completed);
c.Complete(e.Parameter);
}
void c_Completed(object sender, EventArgs e)
{
Completer c = sender as Completer;
foreach (var name in c.Names)
{
People.Add(name);
}
}
}
internal class Completer
{
public event EventHandler<EventArgs> Completed;
public IEnumerable<string> Names { get; set; }
public void Complete(string parameter)
{
if (parameter.StartsWith("d"))
{
Names = new List<string>() { "Dick", "Dave" };
}
else if (parameter.StartsWith("j"))
{
Names = new List<string>() { "Jane", "Joe" };
}
OnCompleted();
}
protected virtual void OnCompleted()
{
var handler = Completed;
if (handler != null) handler(this, EventArgs.Empty);
}
}
In my WPF application, I have a databound TextBox and a databound ItemsControl. The contents of the ItemsControl is determined by the contents of the TextBox. I want to be able to type a value into the TextBox, press tab and enter the first item in the ItemsControl (created from the value in the TextBox). The problem I am having is that the tab action is evaluated before WPF creates the templated items in the ItemsControl. The following code demonstrates this problem:
<Window x:Class="BindingExample.Window1" x:Name="SelfControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:BindingExample" Title="Window1" Height="300" Width="400">
<Window.Resources>
<DataTemplate DataType="{x:Type loc:ChildClass}">
<TextBox Text="{Binding Value}" />
</DataTemplate>
</Window.Resources>
<StackPanel DataContext="{Binding ElementName=SelfControl}" Focusable="False">
<Label Content="Options: A, B, C" />
<TextBox Text="{Binding Object.Value}" />
<ItemsControl Margin="16,0,0,0" ItemsSource="{Binding Object.Children}" Focusable="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBox Text="Box2" />
</StackPanel>
</Window>
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace BindingExample
{
public partial class Window1
{
public static readonly DependencyProperty ObjectProperty = DependencyProperty.Register("Object", typeof(ParentClass), typeof(Window1));
public ParentClass Object
{
get { return GetValue(ObjectProperty) as ParentClass; }
set { SetValue(ObjectProperty, value); }
}
public Window1()
{
InitializeComponent();
Object = new ParentClass();
}
}
public class ParentClass : INotifyPropertyChanged
{
private string value;
public string Value
{
get { return value; }
set { this.value = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Children")); }
}
public IEnumerable<ChildClass> Children
{
get
{
switch (Value.ToUpper())
{
case "A": return new ChildClass[] { "A-1", "A-2", "A-2" };
case "B": return new ChildClass[] { "B-1", "B-2", "B-3" };
case "C": return new ChildClass[] { "C-1", "C-2", "C-2" };
default: return new ChildClass[] { "Unknown" };
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class ChildClass
{
public string Value { get; set; }
public static implicit operator ChildClass(string value) { return new ChildClass { Value = value }; }
}
}
In this code, I would like to type "A" into the first TextBox, press tab, and have the focus shift to the child TextBox with the text "A-1". Instead the focus skips to the TextBox with the text "Box2". How can I achieve the behavior I am looking for here?
Note: As Julien Poulin pointed out, it is possible to make this work by switching the UpdateSourceTrigger on the TextBox to PropertyChanged. This only works, however, if "as you type" binding is acceptable. In my case, I would also like to do the set the value and tab with one keystroke. Is there some way to force the ItemsControl to create its templated items on-demand?
Try to set the UpdateMode of the TextBox to PropertyChanged so the underlying value is set when you type a new value instead of when the TextBox loses focus:
<TextBox Text="{Binding Path=Object.Value, UpdateMode=PropertyChanged}" />
Here is an alternative solution. It's a bit of a hack, but appears to work. Since the templated objects in the ItemsControl aren't created until execution on the main thread pauses, this code catches the tab, updates the binding, and sets a timer to move the focus once the items have a chance to be created.
...
<TextBox Text="{Binding Object.Value}" KeyDown="TextBox_KeyDown" />
...
public partial class Window1
{
private DispatcherTimer timer;
...
private void TextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Tab && e.KeyboardDevice.Modifiers != ModifierKeys.Shift)
{
e.Handled = true;
var textbox = (TextBox)sender;
textbox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
(timer = new DispatcherTimer(
new TimeSpan(100000), // 10 ms
DispatcherPriority.Normal,
delegate
{
textbox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
timer.Stop();
}, Dispatcher)).Start();
}
}
}
The only problem I see with this code is that the ItemsControl might take longer than 10 ms to populate, in which case Tab would jump over those items. Is there any way of detecting whether or not the creation of ItemsControl items has occurred?