Is it possible to bind an Event in a Silverlight DataTemplate? If so, what is the best way to do it?
For example, say you've created a DataTemplate that has a Button in it, like this:
<UserControl.Resources>
<DataTemplate x:Key="MyDataTemplate" >
<Grid>
<Button Content="{Binding ButtonText}" Margin="4" />
</Grid>
</DataTemplate>
</UserControl.Resources>
Then, you apply it to a ListBox ItemTemplate, like this:
<Grid x:Name="LayoutRoot" Background="White">
<ListBox x:Name="lbListBox" ItemTemplate="{StaticResource MyDataTemplate}" />
</Grid>
If you set the ListBox's ItemSource to a list of objects of the class:
public class MyDataClass
{
public string ButtonText{ get; set; }
}
How then do you catch the button click from each button from the DataTemplate in the list? Can you use binding to bind the Click event to a method in "MyButtonClass", like this:
<UserControl.Resources>
<DataTemplate x:Key="MyDataTemplate" >
<Grid>
<Button Click="{Binding OnItemButtonClick}" Content="{Binding ButtonText}" Margin="4" />
</Grid>
</DataTemplate>
</UserControl.Resources>
Would this work? If so, what should I put in the "MyDataClass" to catch the event?
Thanks,
Jeff
There are a couple of options.
One. make a custom control that is bound the data object for that row. On that custom control add the handler for the bound object.
I dont think your binding on the click will work. Sans the Binding Statment and just declare your click to a string.
Add the handler on the page where the control is housed.
Keep in mind that if you bind this way you will only be able to work with the sender of that item (button) and it's properties. If you need to get at specific attributes on an object you maybe better off pursuing the first option.
Small Example demonstrating the functionality by adding 10 buttons to a list box with click events. HTH
DataTemplate XAML
<UserControl.Resources>
<DataTemplate x:Name="MyDataTemplate">
<Grid>
<Button Click="Button_Click" Content="{Binding ItemText}"/>
</Grid>
</DataTemplate>
</UserControl.Resources>
ListBox XAML
<ListBox x:Name="ListBoxThingee" ItemTemplate="{StaticResource MyDataTemplate}"/>
Code Behind (I just plugged this all into the page.xaml file
public class MyClass
{
public string ItemText { get; set; }
}
public partial class Page : UserControl
{
ObservableCollection<MyClass> _Items;
public Page()
{
InitializeComponent();
_Items = new ObservableCollection<MyClass>();
for (int i = 0; i < 10; i++)
{
_Items.Add(new MyClass() {ItemText= string.Format("Item - {0}", i)});
}
this.ListBoxThingee.ItemsSource = _Items;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Button _b = sender as Button;
if (_b != null)
{
string _s = _b.Content as string;
MessageBox.Show(_s);
}
}
}
What I would do is create a button that uses the command pattern for click handling. In the .NET 4 framework you can bind commands to those that exist on the view model.
Related
I have an application where the main window contains a user control, and inside that user control are items stored in an ItemsControl. Each item can be removed by clicking an 'x' button.
The problem I am facing is that although the Keyboard focus is initially set to the user control, when you remove an item, focus is then transferred to the main window, instead of back to the user control?
Is there a way I can fix this without having to add code behind to manually store/retrieve/set focus after the click?
I have lots of these buttons within my application and I'm trying to avoid having to add code all over the place to manage returning the Focus.
I have created a very simple example to show the issue :
<UserControl x:Class="WpfApp28.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding}" />
<Button Content="x"
Width="20"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="Button_Click" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
public partial class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
Focusable = true;
Loaded += MyControl_Loaded;
}
private void MyControl_Loaded(object sender, RoutedEventArgs e)
{
Keyboard.Focus(this);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe && fe.DataContext is string item)
{
(DataContext as ObservableCollection<string>).Remove(item);
}
}
}
<Window x:Class="WpfApp28.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp28"
Title="MainWindow" Height="450" Width="800">
<local:MyControl DataContext="{Binding Items}" />
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
public MainWindow()
{
Items.Add("hello");
Items.Add("there");
Items.Add("world");
DataContext = this;
InitializeComponent();
DispatcherTimer t = new DispatcherTimer();
t.Interval = TimeSpan.FromMilliseconds(250);
t.Tick += T_Tick;
t.Start();
}
private void T_Tick(object? sender, EventArgs e)
{
Title = Keyboard.FocusedElement?.GetType().ToString() ?? "NULL";
}
}
The reason that the keyboard focus moves to the hosting Window is obvious once you understand how WPF handles focus. It's important to know that WPF uses scopes in which the focus traverses the elements.
There can be multiple focus scopes allowing multiple elements to remain focused simultaneously.
By default, the hosting Window defines a focus scope. Since it is the only focus scope, it is global (the scope of the complete visual tree).
What happens in your code in short:
The Button receives the focus via mouse click
The click handler removes the clicked item and therefore the clicked Button from the visual tree
WPF moves focus back to the focus scope root, which is the MainWindow in your case
You have multiple options to prevent the focus from being moved back to the focus root. Some involve code-behind.
The following examples show how to move the focus back to the parent UserControl. But it could be any element as well:
You can configure the Button (the element that "steals" the current focus) to be not focusable. This only works if the UserControl is already focused:
<DataTemplate>
<Button Content="x"
Focusable="False" />
</DataTemplate>
You can introduce a new focus scope. Since you want the UserControl itself to be focused, you must choose the root element of the UserControl. You can achieve this by using the FocusManager helper class:
<UserControl>
<Grid x:Name="RootPanel"
FocusManager.IsFocusScope="True"
Width="300">
</Grid>
</UserControl>
You can of course register a Button.Click handler or preferably a routed command to move the focus back to the UserControl explicitly. A routed command can be more convenient in most cases. It allows to send a command parameter that makes the code-behind simpler.
Note, since Button.Click is a routed event, you can simply register a Button.Click event handler on the UserControl. This example uses the existing click handler that is used to remove the item from the ItemsControl:
UserControl.xaml
<DataTemplate>
<Button Content="x"
Click="OnButtonClick" />
</DataTemplate>
UserControl.xaml.cs
private void OnButtonClick(object sender, RoutedEventArgs)
{
/* Delete the item */
Keyboard.Focus(this);
}
Final suggested solution
To improve your code and handling of the UserControl you must definitely implement an ItemsSource dependency property and use a routed command to delete the items.
The following example uses the predefined ApplicationCommands.Delete routed command. You will notice how simple the code has become:
MyControl.xaml.cs
public partial class MyControl : UserControl
{
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IList),
typeof(UserControl4), new PropertyMetadata(default));
public MyControl()
{
InitializeComponent();
this.Focusable = true;
}
public override void OnApplyTemplate()
=> Keyboard.Focus(this);
private void DeleteItemCommand_Executed(object sender, ExecutedRoutedEventArgs e)
=> this.ItemsSource.Remove(e.Parameter);
private void DeleteItemCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.ItemsSource.Contains(e.Parameter);
}
MyControl.xaml
<UserControl>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.Delete}"
Executed="DeleteItemCommand_Executed"
CanExecute="DeleteItemCommand_CanExecute" />
</UserControl.CommandBindings>
<Grid x:Name="RootPanel"
FocusManager.IsFocusScope="True">
<StackPanel>
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:UserControl4}, Path=ItemsSource}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding}" />
<Button Content="x"
Command="{x:Static ApplicationCommands.Delete}"
CommandParameter="{Binding}"
Width="20"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
MainWindow.xaml
<Window>
<MyControl ItemsSource="{Binding Items}" />
</Window>
Remarks
You should consider to use a ListBox instead of the pure ItemsControl.
ListBox is an extended ItemsControl. It will significantly improve performance and provides a ScrollViewer by default.
My wpf project follows the MVVM pattern. In my view-model I have an IList of obejcts which I take from database. Every object from this IList has a property which is List.
When I open the view for that view-model, I have an ItemsControl with this property:
ItemsSource="{Binding TheIListOfObjects}"
and the items in that ItemsControl are actually showing information from List.
So, while the user is on the view, an itemscontrol is shown. What I want to do is this: while on the same view, if the user clicks a button, the List is changed. How can I make the ItemsControl to refresh and show the new info?
All you need to have is ObservableCollection type for the property type.
Xaml
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<StackPanel>
<Button Content="Click" Click="Button_Click" />
<ListView ItemsSource="{Binding People}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Window>
Xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private ObservableCollection<Person> _people = new ObservableCollection<Person>();
public ObservableCollection<Person> People
{
get { return _people; }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
People.Add(new Person { Name = "A" });
}
}
public class Person
{
public string Name { get; set; }
}
To make sure that changes in a collection are notified to its bound controls, you must use ObservableCollection<> instead of IList<>
In WPF, once we have data bound a collection to the ItemsSource property of a collection control, we don't refresh the ItemsSource property, or interact with it in any other way. Instead, we work with the data bound property value, so for your example...:
ItemsSource="{Binding TheIListOfObjects}"
... you should manipulate the TheIListOfObjects collection:
TheIListOfObjects = GetNewCollectionItems();
If you have correctly implemented the INotifyPropertyChanged interface in your view model, then your view should update as expected.
I have a tab control which items source I databind to an observable collection. I use data templates to define the visual representation of the tab's headers and content.
When I add an item to the observable collection I get a tab header but no content. When I add a second item to the observable collection I get tab headers and content for both items. So first when the second item is added to the observable collection, the content for the first tab is created. Anyone knows if this is a bug or can explain why it happens? Is there a workaround? I tried using template selector with same result. Below is sample code to reproduce.
I tested this with both .NET 3.5 and 4.0.
XAML:
<Window x:Class="TabDemo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<Button Content="Add new tabitem" Click="OnAdd" />
<TabControl
ItemsSource="{Binding Path=Items}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</StackPanel>
</Window>
Code behind:
public partial class Window1
{
public Window1()
{
InitializeComponent();
Items = new ObservableCollection<int>();
DataContext = this;
}
public ObservableCollection<int> Items { get; set; }
private void OnAdd(object sender, RoutedEventArgs e)
{
Items.Add(_random.Next(100));
}
private readonly Random _random = new Random();
}
If you set SelectedIndex="0" on your TabControl, it will work around this issue. I believe this has to do with a bug coercing the SelectedIndex as items are added/removed.
I have a ParentView that contains a childView
<UserControl ... x:Name="MyParentView">
<Grid>
<sdk:TabControl Name="ContactTabControl">
<sdk:TabItem Header="Contact" Name="CustomerTabItem">
<Grid>
<Views:CustomerView/>
</Grid>
</sdk:TabItem>
</sdk:TabControl>
</Grid>
</UserControl>
Within my CustomerView I would like to bind the Firstname textbox to Parent's DataContext. I have tried this inside the CustomerView:
<TextBox Text={Binding ElementName=MyParentView, Path=DataContext.Firstname} />
I have the feeling that CustomerView won't be able to see its parent at all, hence the ElementName "MyParentView" would never be found.
What is your advice on this?
I've done a similar thing but I just bound it directly to Path considering that if I don't give it explicit data context, it will lookup the hierarchy and find one that matches.
So this should get you what you want:
<TextBox Text={Binding Path=FirstName} />
if you need to specify explicit datacontext you can always do:
<Grid>
<Views:CustomerView DataContext={"CustomContextHere"}/>
</Grid>
An alternative solution to Maverik's is :
1 Define a dependency property in your customer view :
public partial class CustomerView : UserControl
{
public CustomerView()
{
InitializeComponent();
}
public static DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string), typeof(CustomerView), new PropertyMetadata(string.Empty, CustomerView.FirstNameChanged));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
private static void FirstNameChanged(object sender, DependencyPropertyChangedEventArgs e)
{ }
}
2 Modify the customer view's textbox to bind to this dependency property (note the element binding "this")
<UserControl x:Class="SLApp.CustomerView"
x:Name="this"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<TextBox Text="{Binding Path=FirstName, ElementName=this, Mode=TwoWay}"/>
</Grid> </UserControl>
3 Modify the parent view and bind it's DataContext to the new dependency property
<sdk:TabControl Name="ContactTabControl">
<sdk:TabItem Header="Contact" Name="CustomerTabItem">
<Grid>
<local:CustomerView FirstName="{Binding ElementName=ContactTabControl, Path=DataContext}"/>
</Grid>
</sdk:TabItem>
</sdk:TabControl>
4 Set the parent's DataContext
public partial class MyParentView : UserControl
{
public MyParentView()
{
InitializeComponent();
ContactTabControl.DataContext = "A name";
}
}
Voila' it works. Not the most elegant solution but it gets the job done for your scenario
I'm trying to build a UserControl which includes 2 listBoxes. I then wan't to set the itemsSources for the listboxes where I use the UserControl.
As I understand that is what DependencyProperties are for. However I'm not successful in doing this. I do believe it mostly has to do with the timing of initialization. Any pointer on what I'm doing right, how I can make it better and such is welcome.
Here is my user control, I'm learning as I'm going so I guess I could do it better
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<ListBox Name="SET" ItemsSource="{Binding Path=SetCollection}" />
<UniformGrid Rows="4" Grid.Column="1">
<Grid />
<Button Margin="10" Click="selectionBtnClick">--></Button>
<Button Margin="10" Click="removeBtnClick">Remove</Button>
</UniformGrid>
<ListBox Name="SUBSET" ItemsSource="{Binding Path=SubsetCollection}" Grid.Column="2" />
</Grid>
CodeBehind
public partial class SubsetSelectionLists : UserControl
{
public static DependencyProperty SetCollectionProperty = DependencyProperty.Register("SetCollection", typeof(CollectionView), typeof(SubsetSelectionLists));
public static DependencyProperty SubsetCollectionProperty = DependencyProperty.Register("SubsetCollection", typeof(CollectionView), typeof(SubsetSelectionLists));
public CollectionView SetCollection
{
get
{
return (CollectionView) GetValue(SetCollectionProperty);
}
set
{
SetValue(SetCollectionProperty, value);
}
}
public CollectionView SubsetCollection
{
get
{
return (CollectionView)GetValue(SubsetCollectionProperty);
}
set
{
SetValue(SubsetCollectionProperty, value);
}
}
public SubsetSelectionLists()
{
InitializeComponent();
}
private void selectionBtnClick(object sender, RoutedEventArgs e)
{
SUBSET.Items.Add(SET.SelectedItem);
}
private void removeBtnClick(object sender, RoutedEventArgs e)
{
SUBSET.Items.Remove(SUBSET.SelectedItem);
}
}
And the code behind where I use it
public partial class SomeWindow : Window
{
ViewModel vm = new ViewModel();
public SomeWindow()
{
InitializeComponent();
NameOfUserControl.SetCollection = vm.InputView;
NameOfUserControl.SubsetCollection = vm.OutputView;
}
}
Here the SetCollection is set as inputView and then later tied to the DependencyProperty severing the original binding I think. Any idea where I'm going wrong?
Ps. subquestion - As I will be moving from one collection to the other shouldn't I ensure somehow that the collection hold objects off the same type? How could I do that?
First of all you should set the DataContext property in SomeWindow to vm. This will allow very simple binding expression in your SomeWindow.xaml.
public partial class SomeWindow : Window
{
ViewModel vm = new ViewModel();
public SomeWindow()
{
InitializeComponent();
this.DataContext = vm;
}
}
In SomeWindow.xaml:
<local:SubsetSelectionLists
SetCollection="{Binding Path=InputView}"
SubsetCollection ="{Binding Path=OutputView}" />
There are several ways to solve the binding problem in the user control:
Setting the data context
Add the following to the constructor of the user control.
this.DataContext = this;
Wpf binds against the current data context, unless a specific source (e.g. ElementName) has been set.
Use binding with ElementName
You could reference the user control in the binding expressions.
<UserControl x:Class="WpfApplication1.SubsetSelectionLists"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="root">
<Grid>
...
<ListBox Name="SET"
ItemsSource="{Binding Path=SetCollection, ElementName=root}" />
...
<ListBox Name="SUBSET" Grid.Column="2"
ItemsSource="{Binding Path=SubsetCollection, ElementName=root}" />
</Grid>
</UserControl>
Bind to the VM
Another method would be to set the data context of the user control in SomeWindow and bind to the Properties of the VM. You could then remove the dependency properties of the user control.
public partial class SomeWindow : Window
{
ViewModel vm = new ViewModel();
//with properties InputView and OutputView
public SomeWindow()
{
InitializeComponent();
NameOfUserControl.DataContext = vm;
}
}
In the user control:
<Grid>
...
<ListBox Name="SET"
ItemsSource="{Binding Path=InputView}" />
...
<ListBox Name="SUBSET" Grid.Column="2"
ItemsSource="{Binding Path=OutputView}" />
</Grid>