I'm creating a custom WPF control (CheckedListComboBox) that allows a user to select one or more options from a list. Essentially the user opens a dropdown-style control and checks off the items they would like to select. When the user checks or unchecks an option in the list, the main (non-popup) area of the control is updated to reflect the choices the user has made.
Here's an example of the control in action:
I am mostly satisfied with the state of the control, but there is one piece I would like to improve and I'm unsure of how to proceed.
The generation of the text that reflects the user's choice(s) currently relies on calling ToString on each of the selected items. This is fine in the example case above, as all of the objects I've passed to the control are strings. However, if I passed in a custom object that didn't override ToString, I would simply get the fully qualified class of the object (e.g. MyNamespace.MyObject).
What I would like to do is implement something akin to a WPF ComboBox's DisplayMemberPath property, where I can simply specify the property to display in the TextBox area of the control for each selected item.
I could just be lazy and go about this via Reflection, but I'm aware that this may not be the fastest (performance-wise) approach.
Here are the options I've considered so far:
Force all bound items to implement a specific interface - if all items passed to the control implemented an interface, such as ICheckableItem, I could then safely access the same property on each item to populate the TextBox. I'm shying away from this approach as I would like the control to be open about what sort of items it can accept.
Use an ItemsControl to display the text, instead of generating it in the code-behind - theoretically I could maintain a list of checked items privately within the control, bind that list to an ItemsControl within the control itself and then somehow bind to the property determined in DisplayMemberPath. I don't know if this is even possible, as I think it would have to do some sort of double-binding magic to work, and I don't think that is doable. There are also issues with getting the separator (in this case a comma) to appear if I follow this route.
The options I've listed above don't seem to work, and I can't think of any more possible approaches. Can anyone offer any other solutions?
Here's my code so far:
XAML
<UserControl
x:Class="MyNamespace.CheckedListComboBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="30"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid x:Name="MainGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="TextBoxHolderGrid">
<TextBox
x:Name="PART_TextBox"
MaxWidth="{Binding ActualWidth, ElementName=TextBoxHolderGrid}"
Focusable="False"
IsReadOnly="True"
TextWrapping="NoWrap" />
</Grid>
<ToggleButton
x:Name="PART_ToggleButton"
Grid.Column="1"
Margin="-1,0,0,0"
HorizontalAlignment="Right"
ClickMode="Press"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
IsTabStop="False">
<Path
x:Name="CollapsedArrow"
Margin="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 4 4 L 8 0 Z">
<Path.Fill>
<SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
</Path.Fill>
</Path>
</ToggleButton>
<Popup
x:Name="PART_Popup"
Grid.ColumnSpan="2"
MinWidth="{Binding ActualWidth, ElementName=MainGrid}"
MaxHeight="{Binding MaxPopupHeight, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Margin="0,-1,0,0"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Placement="Bottom"
StaysOpen="False">
<Border BorderThickness="1">
<Border.BorderBrush>
<SolidColorBrush Color="{x:Static SystemColors.ControlTextColor}" />
</Border.BorderBrush>
<ScrollViewer x:Name="PART_DropDownScrollViewer" BorderThickness="1">
<ItemsControl
ItemTemplate="{Binding ItemTemplate, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
ItemsSource="{Binding ItemsSource, Mode=TwoWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
KeyboardNavigation.DirectionalNavigation="Contained">
<ItemsControl.Background>
<SolidColorBrush Color="{x:Static SystemColors.ControlLightLightColor}" />
</ItemsControl.Background>
</ItemsControl>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</UserControl>
Code behind
public partial class CheckedListComboBox : UserControl
{
private bool mouseIsOverPopup = false;
public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(CheckedListComboBox), new PropertyMetadata(null));
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IEnumerable<CheckedListObject>), typeof(CheckedListComboBox), new PropertyMetadata(null, ItemsSourcePropertyChanged));
public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(CheckedListComboBox), new PropertyMetadata(false, IsDropDownOpenChanged));
public static readonly DependencyProperty MaxPopupHeightProperty = DependencyProperty.Register("MaxPopupHeight", typeof(double), typeof(CheckedListComboBox), new PropertyMetadata((double)200));
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public IEnumerable<CheckedListObject> ItemsSource
{
get { return (IEnumerable<CheckedListObject>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public bool IsDropDownOpen
{
get { return (bool)GetValue(IsDropDownOpenProperty); }
set { SetValue(IsDropDownOpenProperty, value); }
}
public double MaxPopupHeight
{
get { return (double)GetValue(MaxPopupHeightProperty); }
set { SetValue(MaxPopupHeightProperty, value); }
}
public CheckedListComboBox()
{
InitializeComponent();
}
private static void ItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CheckedListComboBox checkedListComboBox)
{
if (e.OldValue != null && e.OldValue is IEnumerable<CheckedListObject> oldItems)
{
foreach (var item in oldItems)
{
item.PropertyChanged -= checkedListComboBox.SubItemPropertyChanged;
}
}
if (e.OldValue != null && e.OldValue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= checkedListComboBox.ItemsSourceCollectionChanged;
}
if (e.NewValue != null && e.NewValue is IEnumerable<CheckedListObject> newItems)
{
foreach (var item in newItems)
{
item.PropertyChanged += checkedListComboBox.SubItemPropertyChanged;
}
}
if (e.NewValue != null && e.NewValue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += checkedListComboBox.ItemsSourceCollectionChanged;
}
checkedListComboBox.CalculateComboBoxText();
}
}
private void SubItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("IsChecked", StringComparison.OrdinalIgnoreCase))
{
CalculateComboBoxText();
}
}
private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// The bound ItemsSource collection has changed, so we want to unsubscribe any old items
// from the PropertyChanged event, and subscribe any new ones to this event.
if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
if (oldItem is CheckedListObject item)
{
item.PropertyChanged -= SubItemPropertyChanged;
}
}
}
if (e.NewItems != null)
{
foreach (var newItem in e.NewItems)
{
if (newItem is CheckedListObject item)
{
item.PropertyChanged += SubItemPropertyChanged;
}
}
}
// We also want to re-calculate the text in the ComboBox, in case any checked items
// have been added or removed.
CalculateComboBoxText();
}
private void CalculateComboBoxText()
{
var checkedItems = ItemsSource?.Where(item => item.IsChecked);
if (checkedItems?.Any() ?? false)
{
PART_TextBox.Text = string.Join(", ", checkedItems.Select(i => i.Item?.ToString()));
}
else
{
PART_TextBox.Text = "";
}
}
private static void IsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CheckedListComboBox box)
{
if (box.IsDropDownOpen)
{
Mouse.Capture(box, CaptureMode.SubTree);
}
else
{
if (Mouse.Captured?.Equals(box) ?? false)
{
Mouse.Capture(null);
}
}
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (IsDropDownOpen)
{
var textBoxPoint = e.GetPosition(PART_TextBox);
var popupPoint = e.GetPosition(PART_Popup.Child);
var mouseIsOverTextBox = !(textBoxPoint.X < 0 || textBoxPoint.X > PART_TextBox.ActualWidth || textBoxPoint.Y < 0 || textBoxPoint.Y > PART_TextBox.ActualHeight);
var mouseIsOverPopup = !(popupPoint.X < 0 || popupPoint.X > PART_Popup.Child.RenderSize.Width || popupPoint.Y < 0 || popupPoint.Y > PART_Popup.Child.RenderSize.Height);
if (mouseIsOverPopup && !mouseIsOverTextBox)
{
mouseIsOverPopup = true;
}
else
{
mouseIsOverPopup = false;
}
}
else
{
mouseIsOverPopup = false;
}
base.OnMouseMove(e);
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (IsDropDownOpen && !mouseIsOverPopup)
{
IsDropDownOpen = false;
}
base.OnMouseLeftButtonDown(e);
}
}
And here's the wrapper class for each of the items.
public class CheckedListObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private object item;
public CheckedListItem()
{ }
public CheckedListItem(object item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public object Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
}
And finally, here's how I'm using the control:
<p:CheckedListComboBox Grid.Row="2" ItemsSource="{Binding Items}">
<p:CheckedListComboBox.ItemTemplate>
<DataTemplate>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<CheckBox Margin="0,0,6,0" IsChecked="{Binding IsChecked}" />
<TextBlock Grid.Column="1" Text="{Binding Item}" />
</Grid>
</DataTemplate>
</p:CheckedListComboBox.ItemTemplate>
</p:CheckedListComboBox>
Related
Here is the complete Sample code
Using MVVM pattern My requirement is to have a ListView where
If user Taps inside ListView on checkBox Storyboard Animation should play for True False and the ListView binded value should be updated in database. For true the tick should pop up with animation for false the tick should become invisible with animation. Solved as per #Elvis Xia answer
If user taps on ListviewItem Navigate to new page with value
Blueprint
Now I went with Usercontrol creation for the datatemplate. Here I want to identify both events separately user clicking on checkbox or clicking on Item separately. Using ICommand I am creating two Delegates that gets binded to two transparent button which relays tapped event. Dependency of creating transparent buttons and creating delgates while binding them made me think surely there must a better way in which I can utilize MVVM for these events without any code behind.
UserControl XAML
<Button Background="LightBlue" BorderBrush="White" BorderThickness="4" Command="{x:Bind sampleItem.itemTapped}" CommandParameter="{Binding}" HorizontalContentAlignment="Stretch">
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Margin="20" HorizontalAlignment="Center" Text="{x:Bind sampleItem.sampleText}" FontSize="30"/>
<Image Grid.Column="1" Height="60" Width="60" Source="ms-appx:///Assets/check_off.png" HorizontalAlignment="Right"/>
<Image x:Name="image" Grid.Column="1" Height="60" Width="60" Source="ms-appx:///Assets/check_on.png" HorizontalAlignment="Right" Visibility="Collapsed" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<CompositeTransform/>
</Image.RenderTransform>
</Image>
<Button x:Name="btnFav" Grid.Column="1" Height="60" Width="60" HorizontalAlignment="Right" Background="Transparent" Command="{x:Bind sampleItem.favTapped}" CommandParameter="{Binding}">
<Interactivity:Interaction.Behaviors>
<!--<Core:EventTriggerBehavior EventName="Tapped">
<Core:InvokeCommandAction Command="{Binding favTapped}" />
</Core:EventTriggerBehavior>-->
<Core:DataTriggerBehavior Binding="{Binding isFav}" Value="true">
<Media:ControlStoryboardAction Storyboard="{StaticResource StoryboardCheckOn}"/>
</Core:DataTriggerBehavior>
<Core:DataTriggerBehavior Binding="{Binding isFav}" Value="false">
<Media:ControlStoryboardAction Storyboard="{StaticResource StoryboardCheckOff}"/>
</Core:DataTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Button>
</Grid>
</Button>
UserControl XAML codeBehind
MainPageModel sampleItem { get { return this.DataContext as MainPageModel; } }
public MainPageUserControl()
{
this.InitializeComponent();
this.DataContextChanged += (s, e) => this.Bindings.Update();
}
Viewmodel Code
public async Task GetData()
{
for (int i = 0; i < 10; i++)
{
if (i == 3)
sampleList.Add(new MainPageModel { sampleText = "Selected", isFav = true, favTapped= new DelegateCommand<MainPageModel>(this.OnFavTapped), itemTapped= new DelegateCommand<MainPageModel>(this.OnItemTapped)});
else
sampleList.Add(new MainPageModel { sampleText = "UnSelected"+i.ToString(), isFav = null, favTapped = new DelegateCommand<MainPageModel>(this.OnFavTapped), itemTapped = new DelegateCommand<MainPageModel>(this.OnItemTapped) });
}
}
private void OnFavTapped(MainPageModel arg)
{
if (arg.isFav == null) arg.isFav = true;
else
arg.isFav = !arg.isFav;
}
private void OnItemTapped(MainPageModel arg)
{
System.Diagnostics.Debug.WriteLine("Button Value: "+arg.sampleText);
System.Diagnostics.Debug.WriteLine("Selected Item Value: "+selectedItem.sampleText);
}
MainPage Xaml
<Grid Grid.Row="1">
<ListView ItemsSource="{x:Bind ViewModel.sampleList}" IsItemClickEnabled="True" SelectedItem="{Binding ViewModel.selectedItem,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<userControls:MainPageUserControl/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
There must be a better way to achieve the desired result using code behind.
public class ViewMOdel()
{
public ViewModel()
{
favTapped= new DelegateCommand<MainPageModel>(this.OnFavTapped)
itemTapped= new DelegateCommand<MainPageModel>(this.OnItemTapped)
}
public async Task GetData()
{
for (int i = 0; i < 10; i++)
{
if (i == 3)
sampleList.Add(new MainPageModel { sampleText = "Selected", isFav = true});
else
sampleList.Add(new MainPageModel { sampleText = "UnSelected"+i.ToString(), isFav = null});
}
}
private void OnFavTapped(MainPageModel arg)
{
if (arg.isFav == null) arg.isFav = true;
else
arg.isFav = !arg.isFav;
}
private void OnItemTapped(MainPageModel arg)
{
System.Diagnostics.Debug.WriteLine("Button Value: "+arg.sampleText);
System.Diagnostics.Debug.WriteLine("Selected Item Value: "+selectedItem.sampleText);
}
}
Binding should be like this
<Button x:Name="btnFav" Grid.Column="1" Height="60" Width="60" HorizontalAlignment="Right" Background="Transparent" Command="{Binding ElementName=UserControl, Path=Tag.favTapped}" CommandParameter="{Binding}"/>
Update
<ListView ItemsSource="{x:Bind ViewModel.sampleList}" x:Name="Listview"IsItemClickEnabled="True" SelectedItem="{Binding ViewModel.selectedItem,Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<userControls:MainPageUserControl Tag="{Binding DataContext,ElementName=Listview}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Button x:Name="btnFav" Grid.Column="1" Height="60" Width="60" HorizontalAlignment="Right" Background="Transparent" Command="{Binding ElementName=UserControl, Path=Tag.favTapped}" CommandParameter="{Binding}"/>
Update2 using EventTriggerBehavior
favTapped = new DelegateCommand<RoutedEventArgs>(this.OnFavTapped);
private void OnFavTapped(RoutedEventArgs arg)
{
var item = (( arg.OriginalSource )as Button).DataContext as MainPageModel
}
<Button n x:Name="btnFav" Grid.Column="1" Height="60" Width="60" HorizontalAlignment="Right" Background="Transparent" >
<interact:Interaction.Behaviors>
<interactcore:EventTriggerBehavior EventName="Click" >
<interactcore:InvokeCommandAction Command="{Binding ElementName=usercontrol, Path=Tag.favTapped}" />
</interactcore:EventTriggerBehavior>
</interact:Interaction.Behaviors>
</Button>
The DataContext of every item in your project is an instance of MainPageModel class. So the favTapped command should be added to MainPageModel class. And it is a command, so favTapped should be an instance of a new class,which implements ICommand interface.
And if you don't want the animation to show at the page's first load, you can set isFav to bool?. And when the page first loads, set the value of isFav to null, thus it won't trigger the animation action.
Below are the Codes snippets and Demo Link:
ViewModelCommands.cs:
public class ViewModelCommands : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
//if it's a tapped event
if (parameter is TappedRoutedEventArgs)
{
var tappedEvent = (TappedRoutedEventArgs)parameter;
var gridSource = (Grid)tappedEvent.OriginalSource;
var dataContext = (MainPageModel)gridSource.DataContext;
//if tick is true then set to false, or the opposite.
if (dataContext.isFav == null)
{
dataContext.isFav = true;
} else
{
dataContext.isFav = !dataContext.isFav;
}
}
}
}
MainPageModel.cs:
public class MainPageModel:BindableBase
{
public MainPageModel() {
favTapped = new ViewModelCommands();
}
public ViewModelCommands favTapped { get; set; }
private string _sampleText;
public string sampleText
{
get
{
return this._sampleText;
}
set
{
Set(ref _sampleText, value);
}
}
private bool? _isFav;
public bool? isFav
{
get
{
return this._isFav;
}
set
{
Set(ref _isFav, value);
}
}
}
Here is the complete Demo:Demo Project
Update:
When using DelegateCommand, you can add the command Property to MainPageModel.cs and since the DataContext of the items are MainPageModel instances. You can use this.isFav to change the clicked item's value of isFav.
Here are the codes of MainPageModel.cs:
public class MainPageModel : BindableBase
{
private DelegateCommand _favTapped;
public DelegateCommand favTapped
{
get
{
if (_favTapped == null)
{
_favTapped = new DelegateCommand(() =>
{
//Here implements the check on or off logic
this.CheckOnOff();
}, () => true);
}
return _favTapped;
}
set { _favTapped = value; }
}
private void CheckOnOff()
{
if (this.isFav == null)
{
this.isFav = true;
}
else
{
this.isFav = !this.isFav;
}
}
private string _sampleText;
public string sampleText
{
get
{
return this._sampleText;
}
set
{
Set(ref _sampleText, value);
}
}
private bool? _isFav;
public bool? isFav
{
get
{
return this._isFav;
}
set
{
Set(ref _isFav, value);
}
}
}
For Listview item selected
You can use ListView.ItemClick Event. But you should also set IsItemClickEnabled="True",otherwise the event handler won't be fired.
For The subitem of Listview tapped
You can use Tapped Event of userControl.
Here are the Xaml codes, that shows how to register the above two events:
<Grid Grid.Row="1">
<ListView IsItemClickEnabled="True" ItemClick="ListView_ItemClick_1" ItemsSource="{x:Bind ViewModel.sampleList}">
<ListView.ItemTemplate>
<DataTemplate>
<userControls:MainPageUserControl Tapped="MainPageUserControl_Tapped"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
I am still fairly new to c# and WPF so perhaps this is expected but I find it strange and I can't seem to get around it.
I have a window with an area to input information about an event and a submit button along with some datagrids bound to observable collections. The button is bound to a command on my viewmodel and when a new event is entered I make some changes to the observable collections and the datagrids on the view are updated.
The issue is when a cell on one of the datagrids is edited. I have tried binding the celleditended event to a command on my view model without much success so I am trying it a different way by using the code behind event handler. I pass the edit information I need to the viewmodel make the changes to the observable collections in exactly the same way as it works above and can see the change in the observable collection but the view doesn't update. The notifypropertychange doesn't fire and neither does the notifycollectionchange.
So why does the observablecollection seem to only fire a property or collection change to update the view when the event is through a Command?
Full Code follows:
XAML code for the main window view; and the example here is where I got the xcdg:CellEditorBinding bit from.
<Window.Resources>
<DataTemplate x:Key="DateTextblock">
<TextBlock Text="{Binding StringFormat={}{0:D}}"/>
</DataTemplate>
<xcdg:DataGridCollectionViewSource x:Key="HistoryViewSource" Source="{Binding History, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<xcdg:DataGridCollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Date" Direction="Descending"/>
</xcdg:DataGridCollectionViewSource.SortDescriptions>
</xcdg:DataGridCollectionViewSource>
<ViewModel:MainWindowViewModel x:Key="MainWindowCollections"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<!--Account Balances and Spending goals-->
<RowDefinition Height="Auto"/>
<!--History-->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<!--Account Balances-->
<ColumnDefinition x:Name="AccountsWidth" Width="Auto"/>
<!--Categorized spending goals-->
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<xcdg:DataGridControl x:Name="AccountsDataGrid"
ItemsSource="{Binding Accounts, UpdateSourceTrigger=PropertyChanged}"
AutoCreateColumns="False"
SelectionMode="Single"
FontSize="14"
Height="Auto"
Width="Auto">
<xcdg:DataGridControl.Columns>
<xcdg:Column Title="Account" FieldName="Name">
<xcdg:Column.CellEditor>
<xcdg:CellEditor>
<xcdg:CellEditor.EditTemplate>
<DataTemplate>
<TextBox x:Name="AccountText" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnSourceUpdated=True}" Style="{StaticResource MainTextboxStyle}" MinWidth="100"/>
</DataTemplate>
</xcdg:CellEditor.EditTemplate>
</xcdg:CellEditor>
</xcdg:Column.CellEditor>
</xcdg:Column>
<xcdg:Column Title="Balance" FieldName="Balance">
<xcdg:Column.CellEditor>
<xcdg:CellEditor>
<xcdg:CellEditor.EditTemplate>
<DataTemplate>
<xctk:DecimalUpDown Value="{Binding Balance, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnSourceUpdated=True}" Style="{StaticResource DecimalUpDownStyle}"/>
</DataTemplate>
</xcdg:CellEditor.EditTemplate>
</xcdg:CellEditor>
</xcdg:Column.CellEditor>
</xcdg:Column>
</xcdg:DataGridControl.Columns>
</xcdg:DataGridControl>
</Grid>
<xcdg:DataGridControl Grid.Row="1" Height="Auto" x:Name="HistoryDataGrid" ItemsSource="{Binding Source={StaticResource HistoryViewSource}, UpdateSourceTrigger=PropertyChanged}" AutoCreateColumns="False" SelectionMode="Single" FontSize="14" xcdg:Cell.EditEnded="HistoryDataGrid_EditEnded">
<!--<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<cmd:EventToCommand PassEventArgsToCommand="True" Command="{Binding TransactionEditCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>-->
<xcdg:DataGridControl.Columns>
<xcdg:Column Title="ID" FieldName="ID" Visible="False"/>
<xcdg:Column Title="Type" FieldName="Type"/>
<xcdg:Column Title="Amount" FieldName="Amount">
<xcdg:Column.CellContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding StringFormat={}{0:C}, NotifyOnSourceUpdated=True}"/>
</DataTemplate>
</xcdg:Column.CellContentTemplate>
<xcdg:Column.CellEditor>
<xcdg:CellEditor>
<xcdg:CellEditor.EditTemplate>
<DataTemplate>
<xctk:DecimalUpDown Value="{xcdg:CellEditorBinding NotifyOnSourceUpdated=True}" Style="{StaticResource DecimalUpDownStyle}"/>
</DataTemplate>
</xcdg:CellEditor.EditTemplate>
</xcdg:CellEditor>
</xcdg:Column.CellEditor>
</xcdg:Column>
</xcdg:DataGridControl.Columns>
</xcdg:DataGridControl>
</Grid>
The View Code behind file
public partial class MainWindow : Window
{
MainWindowViewModel mainwindowviewmodel = new MainWindowViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
public void HistoryDataGrid_EditEnded(object sender, RoutedEventArgs e)
{
mainwindowviewmodel.HistoryCellEdit(e);
}
}
The View Model code
public class MainWindowViewModel : GalaSoft.MvvmLight.ObservableObject
{
public MainWindowViewModel()
{
Accounts = DatabaseFunctions.getAccountData();
History = DatabaseFunctions.getHistoryData();
}
private ObservableCollection<AccountsModel> accounts;
public ObservableCollection<AccountsModel> Accounts
{
get { return accounts; }
set
{
accounts = value;
RaisePropertyChanged("Accounts");
}
}
private ObservableCollection<HistoryModel> history;
public ObservableCollection<HistoryModel> History
{
get { return history; }
set
{
history = value;
RaisePropertyChanged("History");
History.CollectionChanged += History_CollectionChanged;
}
}
public ICommand HistoryEditCommand
{
get { return new ParamDelegateCommand<RoutedEventArgs>(HistoryCellEdit); }
}
public void HistoryCellEdit(RoutedEventArgs e)
{
UpdateAccountData("Chequing", 200);
}
public void UpdateAccountData(string AccountName, decimal NewBalance)
{
var item = Accounts.FirstOrDefault(i => i.Name == AccountName);
if (item != null)
{
item.Balance = NewBalance;
}
}
void Accounts_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (AccountsModel item in e.NewItems)
item.PropertyChanged += Accounts_PropertyChanged;
if (e.OldItems != null)
foreach (AccountsModel item in e.OldItems)
item.PropertyChanged -= Accounts_PropertyChanged;
}
void History_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (HistoryModel item in e.NewItems)
item.PropertyChanged += History_PropertyChanged;
if (e.OldItems != null)
foreach (HistoryModel item in e.OldItems)
item.PropertyChanged -= History_PropertyChanged;
}
void Accounts_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
void History_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
}
The Accounts Model
public class AccountsModel : ObservableObject
{
private Double id;
public Double ID
{
get { return id; }
set
{
id = value;
RaisePropertyChangedEvent("ID");
}
}
private string name;
public string Name
{
get { return name; }
set
{
name = value;
RaisePropertyChangedEvent("Name");
}
}
private decimal balance;
public decimal Balance
{
get { return balance; }
set
{
balance = value;
RaisePropertyChangedEvent("Balance");
}
}
}
This History Model
public class HistoryModel : ObservableObject
{
#region Properties
private Double id;
public Double ID
{
get { return id; }
set
{
id = value;
RaisePropertyChangedEvent("ID");
}
}
private string type;
public string Type
{
get { return type; }
set
{
type = value;
RaisePropertyChangedEvent("Type");
}
}
private decimal amount;
public decimal Amount
{
get { return amount; }
set
{
amount = value;
RaisePropertyChangedEvent("Amount");
}
}
}
Notify Property Changed
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I am attempting to extend TabControl so that I can add and delete items, I have previously done this by adding a close command to my viewmodel that raises an event and a subscription in the parent viewmodel will remove the item from the collection.
I would like to make this approach more generic and am attempting to implement the ApplicationCommands.Delete command.
ExtendedTabControl.cs
public class ExtendedTabControl : TabControl
{
public static readonly DependencyProperty CanUserDeleteTabsProperty = DependencyProperty.Register("CanUserDeleteTabs", typeof(bool), typeof(ExtendedTabControl), new PropertyMetadata(true, OnCanUserDeleteTabsChanged, OnCoerceCanUserDeleteTabs));
public bool CanUserDeleteTabs
{
get { return (bool)GetValue(CanUserDeleteTabsProperty); }
set { SetValue(CanUserDeleteTabsProperty, value); }
}
public static RoutedUICommand DeleteCommand
{
get { return ApplicationCommands.Delete; }
}
private IEditableCollectionView EditableItems
{
get { return (IEditableCollectionView)Items; }
}
private bool ItemIsSelected
{
get
{
if (this.SelectedItem != CollectionView.NewItemPlaceholder)
return true;
return false;
}
}
private static void OnCanExecuteDelete(object sender, CanExecuteRoutedEventArgs e)
{
((WorkspacesTabControl)sender).OnCanExecuteDelete(e);
}
private static void OnCanUserDeleteTabsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// The Delete command needs to have CanExecute run.
CommandManager.InvalidateRequerySuggested();
}
private static object OnCoerceCanUserDeleteTabs(DependencyObject d, object baseValue)
{
return ((WorkspacesTabControl)d).OnCoerceCanUserAddOrDeleteTabs((bool)baseValue, false);
}
private static void OnExecutedDelete(object sender, ExecutedRoutedEventArgs e)
{
((WorkspacesTabControl)sender).OnExecutedDelete(e);
}
static ExtendedTabControl()
{
Type ownerType = typeof(ExtendedTabControl);
DefaultStyleKeyProperty.OverrideMetadata(ownerType, new FrameworkPropertyMetadata(typeof(ExtendedTabControl)));
CommandManager.RegisterClassCommandBinding(ownerType, new CommandBinding(DeleteCommand, new ExecutedRoutedEventHandler(OnExecutedDelete), new CanExecuteRoutedEventHandler(OnCanExecuteDelete)));
}
protected virtual void OnCanExecuteDelete(CanExecuteRoutedEventArgs e)
{
// User is allowed to delete and there is a selection.
e.CanExecute = CanUserDeleteTabs && ItemIsSelected;
e.Handled = true;
}
#endregion
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
object currentItem = SelectedItem;
int indexToSelect = Items.IndexOf(currentItem) - 1;
if (currentItem != CollectionView.NewItemPlaceholder)
EditableItems.Remove(currentItem);
// This should focus the row and bring it into view.
SetCurrentValue(SelectedItemProperty, Items[indexToSelect]);
}
e.Handled = true;
}
private bool OnCoerceCanUserAddOrDeleteTabs(bool baseValue, bool canUserAddTabsProperty)
{
// Only when the base value is true do we need to validate
// that the user can actually add or delete rows.
if (baseValue)
{
if (!this.IsEnabled)
{
// Disabled TabControls cannot be modified.
return false;
}
else
{
if ((canUserAddTabsProperty && !this.EditableItems.CanAddNew) || (!canUserAddTabsProperty && !this.EditableItems.CanRemove))
{
// The collection view does not allow the add or delete action.
return false;
}
}
}
return baseValue;
}
}
Generic.xaml
<!-- This template explains how to render a tab item with a close button. -->
<DataTemplate x:Key="CloseableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
</DataTemplate>
<Style x:Key="{x:Type local:ExtendedTabControl}" BasedOn="{StaticResource {x:Type TabControl}}" TargetType="{x:Type local:ExtendedTabControl}">
<Setter Property="ItemTemplate" Value="{StaticResource CloseableTabItemHeader}" />
</Style>
This nearly works, I can choose an item and remove it by either hitting my close button or using the delete key. However if I hit the close button of an item that isn't selected, it still removes the selected item. The reason for this behavior is obvious, but I'm not sure how to access the correct object for removal? I also need to assign the indexToSelect found in OnExecutedDelete in a better fashion though I'm comfortable I'll find a solution for this.
ExecutedRoutedEventArgs has property Parameter. Try to set DataContext of TabItem as CommandParameter:
<DataTemplate x:Key="CloseableTabItemHeader">
<DockPanel MinWidth="120">
<Button DockPanel.Dock="Right" Command="ApplicationCommands.Delete" CommandParameter="{Binding .}" Content="X" Cursor="Hand" Focusable="False" FontSize="10" FontWeight="Bold" Height="16" Width="16" />
<TextBlock Padding="0,0,10,0" Text="{Binding DisplayName}" VerticalAlignment="Center" />
</DockPanel>
Then you can access DataContext in OnExecutedDelete:
protected virtual void OnExecutedDelete(ExecutedRoutedEventArgs e)
{
if (ItemIsSelected)
{
object currentItem = e.Parameter ?? SelectedItem;
int indexToSelect = Items.IndexOf(currentItem) - 1;
...
}
e.Handled = true;
}
I've just done something very similar in http://dragablz.net
Two check-ins for managing add and remove of tabs:
https://github.com/ButchersBoy/Dragablz/commit/47bbc302f5ffeaa8c234269ab4ff11bc80f7fa10
https://github.com/ButchersBoy/Dragablz/commit/c1dce0435683db83f163a77ccb9a19b3218b3ca7
I have structured the classes like this for my Universal app. Currently working in the WP8.1 part.
The following classes are put in the shared code. (Hoping to use it in Win8.1)
FolderItemViewer.xaml(UserControl) It is the DataTemplate for a ListView in the MainPage.xaml
FolderCollection class, which is the collection that is bound to the Listview in the Mainpage.xaml of the WP
Now the problem is, I have wired the manipulation events to the datatemplate grids in the FolderItemViewer.xaml to capture the right and left swipe and it is working. Now based on this, I need to update that CollectionItem in FolderCollection class and hence the ListView in Mainpage.xaml.
How do I capture that listview item or the collectionitem bound since the manipulation events lie in the FolderItemViewer class?
Can I get the listview item? Or a call back function to the listlivew item template changed? or somethin like that?
EDIT
Sorry to put in so much code. But really appreciate somebody's help in getting this right.
This is the FolderItemViewer.xaml
<UserControl
x:Class="JusWrite2.FolderItemViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:JusWrite2"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="MainGrid">
<Grid Height="60" Width="380" Margin="0,0,0,1">
<Grid x:Name="ItemGrid" HorizontalAlignment="Left" VerticalAlignment="Center" Width="380" Height="60" Background="Transparent" Canvas.ZIndex="2"
ManipulationMode="TranslateX,System" ManipulationStarted="On_ChannelItem_ManipulationStarted" ManipulationDelta="On_ChannelItem_ManipulationDelta" ManipulationCompleted="OnChannelItemManipulationCompleted">
<TextBlock x:Name="titleTextBlock" Margin="20,0,0,0" Canvas.ZIndex="2" VerticalAlignment="Center" TextAlignment="Left" FontSize="25" >
</TextBlock>
</Grid>
<Grid x:Name="DelGrid" Opacity="0.0" HorizontalAlignment="Right" VerticalAlignment="Center" Height="60" Background="Red" Canvas.ZIndex="-1" Tapped="On_ChannelDelete_Tap" Width="380">
<Button Content="X" FontSize="25" Canvas.ZIndex="-1" VerticalAlignment="Center" HorizontalAlignment="Center" Width="380" BorderThickness="0" />
</Grid>
</Grid>
</Grid>
</UserControl>
code behind
private void OnChannelItemManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
Grid ChannelGrid = (Grid)sender;
Grid mGrid = (Grid)(ChannelGrid.Parent);
Grid DeleteGrid = (Grid)((Grid)(ChannelGrid.Parent)).Children[1];
FolderCollection swipedItem = ChannelGrid.DataContext as FolderCollection;// grid has null value for datacontext
double dist = e.Cumulative.Translation.X;
if (dist < -100)
{
// Swipe left
}
else
{
// Swipe right
}
}
FolderCollection.xaml has two classes in it. FolderItem and FolderCollection
public class FolderItem : INotifyPropertyChanged
{
// variables
public FolderItem()
{
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public int CompletionStatus
{
//code
}
public int Priority
{
//code
}
public string FolderText
{
//code
}
public int PenColor
{
//code
}
public string UUID
{
//code
}
public string CreateUUID()
{
//code
}
}
public class FolderCollection : IEnumerable<Object>
{
private ObservableCollection<FolderItem> folderCollection = new ObservableCollection<FolderItem>();
private static readonly FolderCollection instance = new FolderCollection();
public static FolderCollection Instance
{
get
{
return instance;
}
}
public IEnumerator<Object> GetEnumerator()
{
return folderCollection.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add(FolderItem fItem)
{
folderCollection.Add(fItem);
}
public ObservableCollection<FolderItem> FolderCollectionInstance
{
get
{
return folderCollection;
}
}
}
And this is the MainPage.xaml where I have data bound.
// Resources
<DataTemplate x:Key="StoreFrontTileTemplate">
<local:FolderItemViewer />
</DataTemplate>
<ListView x:Name="FolderListView" ItemsSource="{Binding}"
SelectionMode="None"
ItemTemplate="{StaticResource StoreFrontTileTemplate}"
ContainerContentChanging="ItemListView_ContainerContentChanging">
</ListView>
code behind
//Constructor
FolderListView.DataContext = fc.FolderCollectionInstance;
FolderListView.ItemsSource = fc.FolderCollectionInstance;
private void ItemListView_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
FolderItemViewer iv = args.ItemContainer.ContentTemplateRoot as FolderItemViewer;
if (args.InRecycleQueue == true)
{
iv.ClearData();
}
else if (args.Phase == 0)
{
iv.ShowPlaceholder(args.Item as FolderItem);
// Register for async callback to visualize Title asynchronously
args.RegisterUpdateCallback(ContainerContentChangingDelegate);
}
else if (args.Phase == 1)
{
iv.ShowTitle();
args.RegisterUpdateCallback(ContainerContentChangingDelegate);
}
else if (args.Phase == 2)
{
//iv.ShowCategory();
//iv.ShowImage();
}
// For imporved performance, set Handled to true since app is visualizing the data item
args.Handled = true;
}
private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> ContainerContentChangingDelegate
{
get
{
if (_delegate == null)
{
_delegate = new TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs>(ItemListView_ContainerContentChanging);
}
return _delegate;
}
}
private TypedEventHandler<ListViewBase, ContainerContentChangingEventArgs> _delegate;
The list item should be available as the data context of the grid: ChannelGrid.DataContext.
I'm creating a Key/Value pair editor and would like for the value to have a custom template based on the data type.
<TextBox x:Uid="txtKey" x:Name="txtKey" Grid.Column="1" Grid.Row="0" Text="{Binding ElementName=This, Path=KeyValuePair.Key}" VerticalAlignment="Top"/>
<ContentControl Grid.Column="1" Grid.Row="1"
x:Uid="ContentControl1" x:Name="ContentControl1"
Content="{Binding ElementName=This, Path=KeyValuePair.Value}"
LostFocus="ContentControl1_LostFocus"
DataContextChanged="ContentControl1_DataContextChanged" />
The codebehind for the host class looks like this:
public partial class KeyValueControl : ControlBase
{
private System.Collections.DictionaryEntry _dictionaryEntry;
private KeyValuePairObjectObject _KeyValuePair = new KeyValuePairObjectObject();
private DataTemplate _editorDataTemplate;
private Caelum.Libraries.Ui.Editors.Resources resources = new Editors.Resources();
public DataTemplate EditorDataTemplate
{
get { return _editorDataTemplate; }
set { _editorDataTemplate = value; SendPropertyChanged("EditorDataTemplate"); }
}
public KeyValuePairObjectObject KeyValuePair
{
get { return _KeyValuePair; }
set { _KeyValuePair = value; SendPropertyChanged("KeyValuePair"); }
}
public KeyValueControl()
{
InitializeComponent();
this.DataUpdated += new DataUpdatedHander(KeyValueControl_DataUpdated);
DataContextChanged += new DependencyPropertyChangedEventHandler(KeyValueControl_DataContextChanged);
}
void KeyValueControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
}
public override void Save()
{
base.Save();
}
void KeyValueControl_DataUpdated(object sender, object data)
{
if (Data != null)
{
_dictionaryEntry = (System.Collections.DictionaryEntry)Data;
KeyValuePair.Key = _dictionaryEntry.Key;
KeyValuePair.Value = _dictionaryEntry.Value;
if (KeyValuePair.Value != null)
{
EditorDataTemplate = resources.GetDataTemplate(_dictionaryEntry.Value.GetType());
ContentControl1.ContentTemplate = EditorDataTemplate;
}
}
}
}
DataTemplates are chosen via the resources class:
public DataTemplate GetDataTemplate(Type type)
{
if (type == typeof(string))
{
return TextInlineEditorTemplate;
}
if (type == typeof(bool))
{
return BooleanInlineEditorTemplate;
}
return null;
}
The DataTemplate that is displayed for a string is:
<DataTemplate x:Uid="TextInlineEditorTemplate" x:Key="TextInlineEditorTemplate" >
<Grid>
<TextBox x:Uid="txtTextIET1" x:Name="txtTextIET1" Width="300" Text="{Binding Path=DataContext, Mode=TwoWay, RelativeSource={RelativeSource Self}, BindsDirectlyToSource=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
The data binds OK to the key TextBox (txtKey) and DataTemplate TextBox (txtTextIET1), but changing the value on txtTextIET1 will not trigger the setter on the KeyValuePair property. I've not been able to find any examples of this scenario, so any help would be appreciated.
Didn't this work for you
<DataTemplate x:Uid="TextInlineEditorTemplate" x:Key="TextInlineEditorTemplate" >
<Grid>
<TextBox x:Uid="txtTextIET1" x:Name="txtTextIET1" Width="300" Text="{Binding}" />
</Grid>
</DataTemplate>