WPF Customcontrol, Templates, Inheritance and Dependencyproperties - c#

I've got some troubles with a custom control I need to create. I try to explain you my needs first
I need to have a combobox that permits to check more than one item at time (with checkbox) but I want it to be smart enought to bind to a specific type.
I've found some MultiSelectionComboBox but none reflects my need.
Btw my main problem is that I wish to have a generic class as
public class BaseClass<T> : BaseClass
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable<T>), typeof(BaseClass<T>), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(BaseClass<T>.OnItemsSourceChanged)));
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int i = 0;
//MultiSelectComboBox control = (MultiSelectComboBox)d;
//control.DisplayInControl();
}
public IEnumerable<T> ItemsSource
{
get { return (IEnumerable<T>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
}
public class BaseClass : Control
{
}
and a more context specific item for example
public class MultiCurr : BaseClass<Currency>
{
static MultiCurr()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiCurr), new FrameworkPropertyMetadata(typeof(MultiCurr)));
}
}
In my App.xaml I've defined a resource as
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding ItemsSource}" DisplayMemberPath="Description" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
In my MainWindow I've created an object as
<Grid>
<local:MultiCurr x:Name="test" ItemsSource="{Binding Currencies}"></local:MultiCurr>
</Grid>
and the MainWindow.cs is defined as
public partial class MainWindow : Window, INotifyPropertyChanged
{
private IList currencies;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var lst = new List<Currency>();
for (int i = 0; i < 10; i++)
{
var curr = new Currency
{
ID = i,
Description = string.Format("Currency_{0}", i)
};
lst.Add(curr);
}
Currencies = lst;
}
public IList<Currency> Currencies
{
get
{
return this.currencies;
}
set
{
this.currencies = value;
NotifyPropertyChanged("Currencies");
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And here's the result ...
I was wondering what am I doing wrong? is it possible what am I tring to achieve?
Thanks
UPDATE #1:
I've seen that the main problem is the datacontext of the custom usercontrol
<Application.Resources>
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding **Currencies**}" DisplayMemberPath="{Binding **DisplayMemeberPath**}" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Application.Resources>
If I put ItemsSource as Currency (which is a property of the MainWindow) it shows.
If I put ItemsSource and DisplayMemberPath (which are defined in the BaseClass no.. how can I set the context of the usercontrol to itself?)
UPDATE #2
I've added a GoogleDrive link to the project here if anyone wants to try the solution
Thanks

Combobox is not suitable control for multiselection, because it has given behaviour, that when yo select item, Combobox closes itself. That's why Combobox doest not have SelectionMode property like ListBox. I think that ListBox inside expander is what you need.
Generic Types are not a way to go. WPF handles this different, better way. Take listbox as an example. If you bind listbox.itemssource to generic observable collection, and you try to define e.g ItemTemplate, you get full intellisense when writing bindings and warning if you bind to not existing property. http://visualstudiomagazine.com/articles/2014/03/01/~/media/ECG/visualstudiomagazine/Images/2014/03/Figure8.ashx WPF designer automatically recognizes type parameter of your observable collection. Of cousre you need to specify type of datacontext in your page by using something like this: d:DataContext="{d:DesignInstance search:AdvancedSearchPageViewModel}". However your control dont have to be and shouldn't be aware of type of items.
Following example demonstrates control that meets your requirements:
<Expander>
<Expander.Header>
<ItemsControl ItemsSource="{Binding ElementName=PART_ListBox, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Mode=OneWay}" />
<Run Text=";" />
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Expander.Header>
<Expander.Content>
<ListBox x:Name="PART_ListBox" SelectionMode="Multiple">
<ListBox.ItemsSource>
<x:Array Type="system:String">
<system:String>ABC</system:String>
<system:String>DEF</system:String>
<system:String>GHI</system:String>
<system:String>JKL</system:String>
</x:Array>
</ListBox.ItemsSource>
</ListBox>
</Expander.Content>
</Expander>
I reccomend you to create control derived from ListBox (not usercontrol).
I have hardcoded datatemplates, but you should expose them in your custom dependency properties and use TemplateBinding in you control template. Of course you need to modify expander so it looks like combobox and ListBoxItem style so it looks like CheckBox, but it is ease.

Related

UWP - How to change ItemsControl item's DataTemplate as an item's property changes?

I have an ItemsControl bound to an ItemsSource. Each item can have one of several DataTemplates assigned depending on the value of various properties on the item. These properties can change at runtime, and the DataTemplates need to be swapped out individually. In WPF I was able to do so with the following (partial simplified xaml):
<ItemsControl
ItemsSource="{Binding Items}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="ContentTemplate">
<Setter.Value>
<MultiBinding Converter="{StaticResource RowTemplateConverter}">
<Binding Path="(local:Row.Sum)" />
<Binding Path="(local:Row.Avg)" />
<Binding Path="(local:Row.ShowFlagA)" />
<Binding Path="(local:Row.ShowFlagB)" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
I've run into several issues trying to move this to UWP:
MultiBinding is not supported
To compensate for the above, I tried consolidating the converter logic into a single string property of the Row but the DataTemplate doesn't appear to be assigned. Also the binding syntax I used gives runtime XAML errors, not sure why.
<ItemsControl
ItemsSource="{Binding Items}" >
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<Binding Path="RowTemplate" Converter="{StaticResource RowTemplateConverter}"/>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
DataTemplateSelector and ItemContainerStyleSelectors won't work because they're only evaluated once, and I need the templates updated on various property changes
I've seen some answers here that say to null the above Selectors and re-assign them. This is the closest I've been able to get to my desired behavior, but the performance is poor with dozens of items, and fast changing properties, so I'm unable to use this.
You can write an attached behavior to accomplish this. Alternatively extend e.g. ItemsControl (or a derived type).
The key is to reassign the item container's content in order to invoke the DataTemplateSelector again.
The attacehed property will reset the content to trigger the DataTemplateSelector. Your view model will track the changes of the data items that require to re-evaluate the actual DataTemplate and finally trigger the attached property. This is done by simply assigning the changed item to a view model property that binds to the attached behavior.
First create a template selector by extending DataTemplateSelector:
public class DataItemTemplateSelector : DataTemplateSelector
{
public DataTemplate ActivatedTemplate { get; set; }
public DataTemplate DeactivatedTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
switch (item)
{
case DataItem dataItem when dataItem.IsActivated: return this.ActivatedTemplate;
default: return this.DeactivatedTemplate;
}
}
}
Implement the attached behavior that modifies the container of the changed item:
public class TemplateSelector : DependencyObject
{
public static object GetChangedItem(DependencyObject obj)
{
return (object)obj.GetValue(ChangedItemProperty);
}
public static void SetChangedItem(DependencyObject obj, object value)
{
obj.SetValue(ChangedItemProperty, value);
}
public static readonly DependencyProperty ChangedItemProperty =
DependencyProperty.RegisterAttached("ChangedItem", typeof(object), typeof(TemplateSelector), new PropertyMetadata(default(object), OnChangedItemChanged));
private static void OnChangedItemChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
{
if (!(attachingElement is ItemsControl itemsControl))
{
throw new ArgumentException($"Attaching element must be of type '{nameof(ItemsControl)}'");
}
var container = (itemsControl.ItemContainerGenerator.ContainerFromItem(e.NewValue) as ContentControl);
var containerContent = container.Content;
container.Content = null;
container.Content = containerContent; // Trigger the DataTemplateSelector
}
}
Apply the attached property and bind it to your view model. Also assign the template selector:
<Page>
<Page.Resources>
<local:DataItemTemplateSelector x:Key="TemplateSelector">
<local:DataItemTemplateSelector.ActivatedTemplate>
<DataTemplate x:DataType="local:DataItem">
<TextBlock Text="{Binding Text}" Foreground="Red" />
</DataTemplate>
</local:DataItemTemplateSelector.ActivatedTemplate>
<local:DataItemTemplateSelector.DeactivatedTemplate>
<DataTemplate x:DataType="local:DataItem">
<TextBlock Text="{Binding Text}" Foreground="Black" />
</DataTemplate>
</local:DataItemTemplateSelector.DeactivatedTemplate>
</local:DataItemTemplateSelector>
</Page.Resources>
<Grid>
<ListBox ItemsSource="{x:Bind MainViewModel.DataItems}"
local:TemplateSelector.ChangedItem="{x:Bind MainViewModel.UpdatedItem, Mode=OneWay}"
ItemTemplateSelector="{StaticResource TemplateSelector}" />
</Grid>
</Page>
Finally let the view model track the relevant property changes and set the changed property e.g. to a UpdatedItem property which binds to the attached behavior (see above):
public class MainViewModel : ViewModel, INotifyPropertyChanged
{
public MainViewModel()
{
DataItems = new ObservableCollection<DataItem>();
for (int index = 0; index < 10; index++)
{
DataItem newItem = new DataItem();
// Listen to property changes that are relevant
// for the selection of the DataTemplate
newItem.Activated += OnItemActivated;
this.DataItems.Add(newItem);
}
}
// Trigger the attached property by setting the property that binds to the behavior
private void OnItemActivated(object sender, EventArgs e) => this.UpdatedItem = sender as DataItem
public ObservableCollection<DataItem> DataItems { get; }
private DataItem updatedItem;
public DataItem UpdatedItem
{
get => this.updatedItem;
set
{
this.updatedItem = value;
OnPropertyChanged();
}
}
}
this only updates the container of the item that you select in the view model.
Yep, The DataItemTemplateSelector works when preparing items. it will not response the item's property change even if it has implement INotifyPropertyChanged interface, the better way is use IValueConverter to update the uielement base on the specific property.
For example
public class ImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
object img = null;
switch (value.ToString())
{
case "horizontal":
img = new BitmapImage(new Uri("ms-appx:///Assets/holder1.png"));
break;
case "vertical":
img = new BitmapImage(new Uri("ms-appx:///Assets/holder2.png"));
break;
default:
break;
}
return img;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
For please refer IValueConverter document.

How to dynamically change itemtemplate in UWP?

In WPF you can modify controls using Styles and Templates dynamically with Binding. I see how to do this in UWP directly in the control but I want to apply a template that will change itself based on the binding.
An example would be a button. I have a button that turns on and off a light in this project. The project is already created and running in WPF but needs to be converted to UWP. In the WPF version we have a LightStyle for the button, depending on what type of light it is, we change the Template to look and perform for that light. (For example: we can change the color of some lights, the dimness for some lights, and some lights just turn on and off; but we use the same LightStyle for them all. Very generic, dynamic, and extremely useful.)
How do you do this in UWP? I've searched a minute and figured I would stop here and check while I continue to dig. Keep in mind that this project is pure MVVM and no code behind is used. I don't mind a code behind explanation as long as it's not the only way.
Thanks in advance :)
Here is a sample I've made - XAML:
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Orientation="Horizontal">
<StackPanel.Resources>
<local:MySelector x:Key="MySelector">
<local:MySelector.GreenTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="Green"/>
</DataTemplate>
</local:MySelector.GreenTemplate>
<local:MySelector.RedTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="Red"/>
</DataTemplate>
</local:MySelector.RedTemplate>
</local:MySelector>
</StackPanel.Resources>
<ListView x:Name="ListOfItems" Width="100" ItemTemplateSelector="{StaticResource MySelector}"/>
<StackPanel>
<ToggleSwitch OnContent="GREEN" OffContent="RED" Margin="10" IsOn="{x:Bind IsSwitched, Mode=TwoWay}"/>
<Button Content="Add item" Click="AddClick" Margin="10"/>
</StackPanel>
</StackPanel>
and the code behind:
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaiseProperty(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
private bool isSwitched = false;
public bool IsSwitched
{
get { return isSwitched; }
set { isSwitched = value; RaiseProperty(nameof(IsSwitched)); }
}
public MainPage() { this.InitializeComponent(); }
private void AddClick(object sender, RoutedEventArgs e)
{
ListOfItems.Items.Add(new ItemClass { Type = isSwitched ? ItemType.Greed : ItemType.Red, Text = "NEW ITEM" });
}
}
public enum ItemType { Red, Greed };
public class ItemClass
{
public ItemType Type { get; set; }
public string Text { get; set; }
}
public class MySelector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
switch ((item as ItemClass).Type)
{
case ItemType.Greed:
return GreenTemplate;
case ItemType.Red:
default:
return RedTemplate;
}
}
public DataTemplate GreenTemplate { get; set; }
public DataTemplate RedTemplate { get; set; }
}
Generally you can choose various switches for your selector, it depends on your needs. In above example I'm switching the template basing on the item's property, here is a good example how to switch on item's type.
Here's the answer that I am using that works for my given situation. Basically you have to use a VisualStateTrigger and create the trigger manually via code. There are various triggers you can use and many built in but for this situation I had to, or at least I think I had to, write one manually.
Here's the trigger code.
public class StringComparisonTrigger : StateTriggerBase
{
private const string NotEqual = "NotEqual";
private const string Equal = "Equal";
public string DataValue
{
get { return (string)GetValue(DataValueProperty); }
set { SetValue(DataValueProperty, value); }
}
public static readonly DependencyProperty DataValueProperty =
DependencyProperty.Register(nameof(DataValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(Equal, (s, e) =>
{
var stringComparisonTrigger = s as StringComparisonTrigger;
TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.TriggerValue, (string)e.NewValue);
}));
public string TriggerValue
{
get { return (string)GetValue(TriggerValueProperty); }
set { SetValue(TriggerValueProperty, value); }
}
public static readonly DependencyProperty TriggerValueProperty =
DependencyProperty.Register(nameof(TriggerValue), typeof(string), typeof(StringComparisonTrigger), new PropertyMetadata(NotEqual, (s, e) =>
{
var stringComparisonTrigger = s as StringComparisonTrigger;
TriggerStateCheck(stringComparisonTrigger, stringComparisonTrigger.DataValue, (string)e.NewValue);
}));
private static void TriggerStateCheck(StringComparisonTrigger elementTypeTrigger, string dataValue, string triggerValue)
=> elementTypeTrigger.SetActive(dataValue == triggerValue);
}
This, since inheriting from StateTriggerBase can be used in the VisualStateTriggers group as I will post below. What I didn't know is that any dependency property you write can be used in the XAML and there's no interfaces or anything in the trigger to make it work. The only line of code that fires the trigger is 'SetActive(bool value)' that you must call whenever you want the state to change. By making dependency properties and binding in the XAML you can fire the SetActive whenever the property is changed and therefore modify the visual state.
The DataTemplate is below.
<DataTemplate x:Key="LightsButtonTemplate">
<UserControl>
<StackPanel Name="panel">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<DataTriggers:StringComparisonTrigger DataValue="{Binding Type}"
TriggerValue="READ" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="panel.(UIElement.Background)"
Value="Red" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock Text="{Binding Type}" />
<TextBlock Text="{Binding LightStateViewModel.On}" />
</StackPanel>
</UserControl>
</DataTemplate>
And finally using you can use the DataTemplate anywhere but I am using it in an ItemsControl that is bound to a list of LightViewModels.
<ScrollViewer Grid.Row="1">
<ItemsControl ItemsSource="{Binding LightViewModels}"
ItemTemplate="{StaticResource LightsButtonTemplate}" />
</ScrollViewer>
Obviously this isn't the template design I want for the light buttons but this is all I've done to understand and now implement dynamic templates. Hopefully this helps someone else coming from WPF.
The custom trigger class deriving from StateTriggerBase can do and bind anyway you want it to and all you need to do is call SetActive(true) or SetActive(false) whenever you wish to update that trigger. When it's true the VisualState using that trigger will be active.

SelectedItem doesn't update properly when trying to share items between views

I have a scenario where the same collection of items can be viewed in different ways. That is, we have multiple visual representations for the same data. In order to keep our application visually clean you can only view one of these views at a time. The problem I'm having is that if you change the selected item while viewing View #1 then when you switch to View #2 the selected item isn't updating properly.
My steps for reproducing:
On View #1 select Item #1.
Toggle to View #2 - at this point Item #1 is selected
Scroll down to "Item #200" and select it
Toggle back to View #1
Item #1 will still be highlighted and if you scroll down to Item #200 it is also highlighted
It seems like when the listbox is collapsed the selection changes aren't being picked up. What am I missing? Is it expected that the PropertyChanged events won't update the UI elements if they aren't visible?
I have a very simplified version of my code below. Basically, I have a shared array that is being bound to two different ListBox controls.
XAML:
<Window x:Class="SharedListBindingExample.MainWindow"
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"
xmlns:local="clr-namespace:SharedListBindingExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" x:Name="listBox1" ItemsSource="{Binding List1}">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
<DataTemplate DataType="{x:Type local:SharedListItem}">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Background="Red" />
<Label Grid.Row="1" Content="{Binding Name}" />
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<ListBox Grid.Row="0" x:Name="listBox2" ItemsSource="{Binding List2}" Background="AliceBlue" Visibility="Collapsed">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}"/>
</Style>
<DataTemplate DataType="{x:Type local:SharedListItem}">
<Label Content="{Binding Name}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"/>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<Button Grid.Row="1" Click="Button_Click">Toggle View</Button>
</Grid>
</Window>
Code Behind:
using System.Windows;
namespace SharedListBindingExample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (listBox1.Visibility == Visibility.Collapsed)
{
listBox1.Visibility = Visibility.Visible;
listBox2.Visibility = Visibility.Collapsed;
}
else
{
listBox2.Visibility = Visibility.Visible;
listBox1.Visibility = Visibility.Collapsed;
}
}
}
}
ViewModel:
using System.Collections.Generic;
namespace SharedListBindingExample
{
public class TwoPropertiesForSameListViewModel
{
private readonly List<SharedListItem> _sharedList;
public TwoPropertiesForSameListViewModel()
{
_sharedList = new List<SharedListItem>();
for (int i = 0; i < 300; i++)
{
_sharedList.Add(new SharedListItem($"Item #{i}"));
}
}
public IEnumerable<SharedListItem> List1
{
get
{
return _sharedList;
}
}
public IEnumerable<SharedListItem> List2
{
get
{
return _sharedList;
}
}
}
}
SharedListItem:
using System.ComponentModel;
namespace SharedListBindingExample
{
public class SharedListItem : INotifyPropertyChanged
{
private bool _isSelected;
public SharedListItem(string name)
{
Name = name;
}
public string Name { get; set; }
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I believe you need to use a CollectionViewSource object between the two different views to keep the selected item in sync. I do something similar in my own application.
I have one control which defines a CollectionViewSource resource in the Xaml:
<UserControl.Resources>
<CollectionViewSource x:Key="DataView" />
</UserControlResources>
The control also has a DependencyProperty for the CollectionViewSource that allows it to be Data Bound to other controls:
public static readonly DataViewProperty =
DependencyProperty.Register( "DataView", typeof( CollectionViewSource ), typeof( YourControlType ), new PropertyMetadata( null ) );
public CollectionViewSource DataView {
get { return (CollectionViewSource) GetProperty( DataViewProperty); }
set { SetProperty( DataViewProperty, value );
}
Then in the components constructor, after calling InitializeComponent, you have to execute code like this:
public MyUserControl() {
InitializeComponent();
DataView = FindResource( "DataView" ) as CollectionViewSource;
DataView.Source = YourObservableCollection;
}
In the other view(s) where you want to share this object, you create a new CollectionViewSource DependencyProperty. This allows you to bind the two proeprties to each other in the window that has the different views of your data. In my second control, I have another ObservableCollection object property, but it is not initialized in the control's constructor. What I do is in the control's Loaded event handler, I set that ObservableCollection property's value to the value of the CollectionViewSource object's Source property. That is:
if ( DataCollection == null && DataView != null ) {
DataCollection = (ObservableCollection<DataType>) DataView.Source;
DataGrid.ItemsSource = DataView.View;
}
After this, both controls share the same ObservableCollection and the same CollectionViewSource. It's the CollectionViewSource that keeps the two control's selected item in sync.
Obviously, you can share that CollectionViewSource object across as many views as you like. One control has to declare the object, the others have to share it.
When using WPF, it is recommended to ditch IEnumerables for ObservableCollection or ICollectionView. You can find more details about these collections on MSDN, but my suggestion is to bind SelectedItem of one ListBox to another in XAML (by SelectedItem = {Binding ElementName='yourList', Path='SelectedItem'}), so that when one changes, the other one will respond (you should do this for both lists). I have never tried this cyclic binding myself, but I think it will work fine in your case.

WPF Binding Cannot find source for binding with reference

I've been following this answer to expose some properties of my user-control.
The problem being that the binding doesn't find the source and I don't understand how to do it properly.
XAML:
<UserControl x:Class="Project.UI.Views.ucFilterDataGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Project.UI.Views"
xmlns:watermark="clr-namespace:Project.UI.Watermark"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<StackPanel.Resources>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Margin" Value="0,0,00,30"/>
</Style>
</StackPanel.Resources>
<AdornerDecorator>
<TextBox Name="SearchTextBox">
<watermark:WatermarkService.Watermark>
<TextBlock Name="waterMarkText"
Text="{Binding Path=WatermarkContent,
RelativeSource={RelativeSource FindAncestor,
AncestorType=local:ucFilterDataGrid}}"
HorizontalAlignment="Center" >
</TextBlock>
</watermark:WatermarkService.Watermark>
</TextBox>
</AdornerDecorator>
<DataGrid Name="Results">
</DataGrid>
</StackPanel>
CS:
namespace Project.UI.Views
{
/// <summary>
/// Interaction logic for ucFilterDataGrid.xaml
/// </summary>
public partial class ucFilterDataGrid : UserControl
{
public ucFilterDataGrid()
{
InitializeComponent();
}
public string WatermarkContent
{
get { return GetValue(WatermarkContentProperty).ToString(); }
set { SetValue(WatermarkContentProperty, value); }
}
public static readonly DependencyProperty WatermarkContentProperty = DependencyProperty.Register("WatermarkContent", typeof(string), typeof(ucFilterDataGrid), new FrameworkPropertyMetadata(string.Empty));
}
}
Window:
<Grid>
<local:ucFilterDataGrid Margin="301,34,31,287" WatermarkContent="MyTest"/>
</Grid>
The result will be a blank TextBlock. If I just remove it from my watermark UserControl and put it on the same level as the DataGrid, it will work is intended.
The problem here is your TextBlock is set as a value of an attached property, here it is:
<watermark:WatermarkService.Watermark>
<TextBlock ...>
</TextBlock>
</watermark:WatermarkService.Watermark>
watermark:WatermarkService.Watermark is an attached property. Its value is just an object in memory and detached from the visual tree. So you cannot use Binding with RelativeSource or ElementName. You need some proxy to bridge the disconnection. The Source will be used for Binding, the code you should try is as follow:
<TextBox Name="SearchTextBox">
<TextBox.Resources>
<DiscreteObjectKeyFrame x:Key="proxy"
Value="{Binding Path=WatermarkContent,
RelativeSource={RelativeSource FindAncestor,
AncestorType=local:ucFilterDataGrid}}"/>
</TextBox.Resources>
<watermark:WatermarkService.Watermark>
<TextBlock Name="waterMarkText"
Text="{Binding Value, Source={StaticResource proxy}}"
HorizontalAlignment="Center" >
</TextBlock>
</watermark:WatermarkService.Watermark>
</TextBox>
I made something similar the other day, and if I remember correctly. You will have to derive from the INotifyPropertyChanged interface and tell the component that the property has changed whenever you update the WatermarkContent. Otherwise the xaml (view) will not know when you change the Text, and the binding wont update.
Here is what you can try out
using System.ComponentModel;
public partial class ucFilterDataGrid : UserControl, INotifyPropertyChanged
{
public static readonly DependencyProperty WatermarkContentProperty = DependencyProperty.Register("WatermarkContent", typeof(string), typeof(ucFilterDataGrid), new FrameworkPropertyMetadata(string.Empty));
public event PropertyChangedEventHandler PropertyChanged;
public ucFilterDataGrid()
{
InitializeComponent();
}
public string WatermarkContent
{
get { GetValue(WatermarkContentProperty).ToString(); }
set {
SetValue(WatermarkContentProperty, value);
RaisePropertyChanged();
}
}
protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I've added the INotifyPropertyChanged and raises the event each time the WatermarkContent is changed.
Hope it helped!

Updating the SelectedItem in an ItemsControl (ComboBox) in a UserControl

Before any item in a ComboBox is selected, its SelectedItemis null and the ComboBox itself is visually blank. Once something is selected, there doesn't seem to be any way for the user to select "the absence of a selection" (though it can be done by setting SelectedItem to null in code).
My ComboBoxes are bound to ObservableCollections of my objects. I don't want to add a "special" first null-like object to the front of every ObservableCollection. So I'm taking this opportunity to learn a bit about writing a UserControl.
The problem is SelectedItem doesn't work the way it normally does. That is, the ComboBox is nicely bound to a backing ObservableCollection, but picking something from the ComboBox doesn't update the SelectedItem it's supposed to be bound to.
I feel like I need to be passing along some information from the ComboBox in the UserControl to...somewhere. Am I on the right track? What should I be googling for?
C#:
public partial class ClearableComboBox : UserControl
{
public ClearableComboBox()
{
InitializeComponent();
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)base.GetValue(ItemsSourceProperty); }
set { base.SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource",
typeof(IEnumerable),
typeof(ClearableComboBox));
public object SelectedItem
{
get { return (object)base.GetValue(SelectedItemProperty); }
set { base.SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem",
typeof(object),
typeof(ClearableComboBox));
public string DisplayMemberPath
{
get { return (string)base.GetValue(DisplayMemberPathProperty); }
set { base.SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
DependencyProperty.Register("DisplayMemberPath",
typeof(string),
typeof(ClearableComboBox));
private void Button_Click(object sender, RoutedEventArgs e)
{
comboBox.SelectedItem = null;
}
}
XAML:
<UserControl x:Class="MyProj.ClearableComboBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="root">
<DockPanel>
<Button DockPanel.Dock="Left" Click="Button_Click" ToolTip="Clear">
<Image Source="pack://application:,,,/img/icons/silk/cross.png" Stretch="None" />
</Button>
<ComboBox
Name="comboBox"
ItemsSource="{Binding ElementName=root, Path=ItemsSource}"
SelectedItem="{Binding ElementName=root, Path=SelectedItem}"
DisplayMemberPath="{Binding ElementName=root, Path=DisplayMemberPath}" />
</DockPanel>
</UserControl>
Usage:
<wpfControl:ClearableComboBox ItemsSource="{Binding Path=Things}"
DisplayMemberPath="SomeProperty"
SelectedItem="{Binding Path=SelectedThing}" />
// Picking a Thing doesn't update SelectedThing :(
Since combobox derives from Selector class which in turn derives from ItemsControl. So, by deriving from UserControl you are devoiding your combobox with properties of Selector class which might internally handle the Selection thing for you. so, i would suggest instead of deriving it from UserControl, you should derive it from Combobox like this -
public partial class ClearableComboBox : ComboBox
So, that ways you won't have to override the ItemsSource, DisplayMemberPath etc. in your class since it s already present in the ComboBox class. You can always extend your class further to provide addidtional features which is in your case setting the SelectedItem to null on some button click. Hope this is what you want..
EDIT (Custom Control)
Creating a Custom Control is your answer here, to get started if you are not aware of it, look at this for start - http://www.wpftutorial.net/HowToCreateACustomControl.html
When you create a Custom Control say CustomControl1, replace the template for CustomControl1 in your Generic.xaml file with this one -
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<Button Name="btn" DockPanel.Dock="Left" ToolTip="Clear" Width="20">
<Image Source="pack://application:,,,/img/icons/silk/cross.png" Stretch="None" />
</Button>
<ComboBox Name="comboBox"
ItemsSource="{TemplateBinding ItemsSource}"
SelectedItem="{TemplateBinding SelectedItem}"
DisplayMemberPath="{TemplateBinding DisplayMemberPath}" />
</DockPanel>
</Border>
</ControlTemplate>
By default your CustomControl1 class will be derived from Control. Replace it to derive from class ComboBox so that you don't have declare DP's yet over again like this and copy paste this code there -
public class CustomControl1 : ComboBox
{
private Button clearButton;
private ComboBox comboBox;
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
clearButton = GetTemplateChild("btn") as Button;
comboBox = GetTemplateChild("comboBox") as ComboBox;
clearButton.Click += new RoutedEventHandler(clearButton_Click);
}
private void clearButton_Click(object sender, RoutedEventArgs e)
{
comboBox.SelectedItem = null;
}
}
Now, your CustomControl1 class is ready for use in your other xaml files like this -
<local:CustomControl1 ItemsSource="{Binding YourSource}"
SelectedItem="{Binding YourSelectedItem}"
Height="50" Width="200"/>
I chose to handle the key press event on the combo box and handle the escape key press to clear out the combo box's SelectedItem.
I think there's a better way, develop a wrapper/Adorner for ComboBox, that adds a button next to the ComboBox and wipe the selection on click.

Categories

Resources