WPF Combobox SelectedItem not in Itemssource - wpf

How do you get the SelectedItem of a ComboBox to show even when it is not in the ItemsSource?
just as a simple example...
Suppose I have a "Class" object with a "Teacher" property on it.
public class Class: INotifyPropertyChanged
{
private Individual _teacher
public Individual Teacher
{
get { return _teacher; }
set
{
teacher = value;
RaisePropertyChanged("Teacher");
}
}
...
}
On the "Maintain Classes" GUI, there is a ComboBox to select a Teacher, and I only want active individuals to show up in the ComboBox. And I don't want users to be able to type free form text into the ComboBox. To achieve this, I bind ItemsSource to a collection in my ViewModel that only includes active individuals and the SelectedItem bound to a Teacher property of my "Class" object.
public class MaintainClasses_ViewModel:INotifyPropertyChanged
{
private ObservableCollection<Individual> _activeIndividuals
= GetAllActiveIndividuals();
public ObservableCollection<Individual> ActiveIndividuals
{
get { return _activeIndividuals
}
public Class SelectedClass
{
get;
set;
}
}
with the xaml for my ComboBox being...
<ComboBox ItemsSource="{Binding ActiveIndividuals}"
SelectedItem="{Binding SelectedClass.Teacher}" />
Now suppose I open the "Maintain Classes" GUI for a class where the teacher that has already been saved with is now inactive. Now... I want only active individuals to show up in the combobox -PLUS the teacher that was previously selected (even though they are now inactive and NOT in the ItemsSource).
Currently, the only way I have found to do this is to add the Inactive Individual to the collection and raise the PropertyChanged event for the collection. However, I would really like to archive this result without adding things to the collection. Preferably some method that uses xaml, selectors, and/or converters.

Here is what I've been using, I hope it Helps:
ComboBoxAdaptor.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
namespace Adaptors
{
[ContentProperty("ComboBox")]
public class ComboBoxAdaptor : ContentControl
{
#region Protected Properties
protected bool IsChangingSelection
{get; set;}
protected ICollectionView CollectionView
{get; set;}
#endregion
#region Dependency Properties
public static readonly DependencyProperty ComboBoxProperty =
DependencyProperty.Register("ComboBox", typeof(ComboBox), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(new PropertyChangedCallback(ComboBox_Changed)));
public ComboBox ComboBox
{
get { return (ComboBox)GetValue(ComboBoxProperty);}
set { SetValue(ComboBoxProperty, value);}
}
public static readonly DependencyProperty NullItemProperty =
DependencyProperty.Register("NullItem", typeof(object), typeof(ComboBoxAdaptor),
new PropertyMetadata("(None)");
public object NullItem
{
get {return GetValue(NullItemProperty);}
set {SetValue(NullItemProperty, value);}
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(new PropertyChangedCallback(ItemsSource_Changed)));
public IEnumerable ItemsSource
{
get {return (IEnumerable)GetValue(ItemsSourceProperty);}
set {SetValue(ItemsSourceProperty, value);}
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(ComboBoxAdaptor),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(SelectedItem_Changed)));
public object SelectedItem
{
get {return GetValue(SelectedItemProperty);}
set {SetValue(SelectedItemProperty, value);}
}
public static readonly DependencyProperty AllowNullProperty =
DependencyProperty.Register("AllowNull", typeof(bool), typeof(ComboBoxAdaptor),
new PropertyMetadata(true, AllowNull_Changed));
public bool AllowNull
{
get {return (bool)GetValue(AllowNullProperty);}
set {SetValue(AllowNullProperty, value);}
}
#endregion
#region static PropertyChangedCallbacks
static void ItemsSource_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
adapter.Adapt();
}
static void AllowNull_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
adapter.Adapt();
}
static void SelectedItem_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ComboBoxAdaptor adapter = (ComboBoxAdaptor)d;
if (adapter.ItemsSource != null)
{
//If SelectedItem is changing from the Source (which we can tell by checking if the
//ComboBox.SelectedItem is already set to the new value), trigger Adapt() so that we
//throw out any items that are not in ItemsSource.
object adapterValue = (e.NewValue ?? adapter.NullItem);
object comboboxValue = (adapter.ComboBox.SelectedItem ?? adapter.NullItem);
if (!object.Equals(adapterValue, comboboxValue))
{
adapter.Adapt();
adapter.ComboBox.SelectedItem = e.NewValue;
}
//If the NewValue is not in the CollectionView (and therefore not in the ComboBox)
//trigger an Adapt so that it will be added.
else if (e.NewValue != null && !adapter.CollectionView.Contains(e.NewValue))
{
adapter.Adapt();
}
}
}
#endregion
#region Misc Callbacks
void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ComboBox.SelectedItem == NullItem)
{
if (!IsChangingSelection)
{
IsChangingSelection = true;
try
{
int selectedIndex = ComboBox.SelectedIndex;
ComboBox.SelectedItem = null;
ComboBox.SelectedIndex = -1;
ComboBox.SelectedIndex = selectedIndex;
}
finally
{
IsChangingSelection = false;
}
}
}
object newVal = (ComboBox.SelectedItem == null? null: ComboBox.SelectedItem);
if (!object.Equals(SelectedItem, newVal)
{
SelectedItem = newVal;
}
}
void CollectionView_CurrentChanged(object sender, EventArgs e)
{
if (AllowNull && (ComboBox != null) && (((ICollectionView)sender).CurrentItem == null) && (ComboBox.Items.Count > 0))
{
ComboBox.SelectedIndex = 0;
}
}
#endregion
#region Methods
protected void Adapt()
{
if (CollectionView != null)
{
CollectionView.CurrentChanged -= CollectionView_CurrentChanged;
CollectionView = null;
}
if (ComboBox != null && ItemsSource != null)
{
CompositeCollection comp = new CompositeCollection();
//If AllowNull == true, add a "NullItem" as the first item in the ComboBox.
if (AllowNull)
{
comp.Add(NullItem);
}
//Now Add the ItemsSource.
comp.Add(new CollectionContainer{Collection = ItemsSource});
//Lastly, If Selected item is not null and does not already exist in the ItemsSource,
//Add it as the last item in the ComboBox
if (SelectedItem != null)
{
List<object> items = ItemsSource.Cast<object>().ToList();
if (!items.Contains(SelectedItem))
{
comp.Add(SelectedItem);
}
}
CollectionView = CollectionViewSource.GetDefaultView(comp);
if (CollectionView != null)
{
CollectionView.CurrentChanged += CollectionView_CurrentChanged;
}
ComboBox.ItemsSource = comp
}
}
#endregion
}
}
How To Use It In Xaml
<adaptor:ComboBoxAdaptor
NullItem="Please Select an Item.."
ItemsSource="{Binding MyItemsSource}"
SelectedItem="{Binding MySelectedItem}">
<ComboBox Width="100" />
</adaptor:ComboBoxAdaptor>
Some Notes
If SelectedItem changes to a value not in the ComboBox, it will be added to the ComboBox (but not the ItemsSource). The next time SelectedItem is changed via Binding, any items not in ItemsSource will be removed from the ComboBox.
Also, the ComboBoxAdaptor allows you to insert a Null item into the ComboBox. This is an optional feature that you can turn off by setting AllowNull="False" in the xaml.

Related

Custom WPF DataGrid to select one or multiple rows manually from ViewModel

I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.

WPF TabItem state lost when selected tab changes

I have a TabControl with multiple tabs. Each tab has a multiline TextBox in it.
So in the visible textbox in Tab 1, the insertion point starts out at the beginning of line 1.
I move the insertion point to the end of line 2.
I change the selected tab to Tab 2, and then back to Tab 1.
The insertion point in the Tab 1 TextBox is always back at the start of line one.
Both tabs are being populated by viewmodels in a list:
<TabControl ItemsSource="{Binding TabItemViewModelList}">
<TabControl.Resources>
<DataTemplate DataType="TabItemViewModel">
<Grid>
<TextBox Text="{Binding Text}" />
</Grid>
</DataTemplate>
</TabControl.Resources>
<TabControl>
That's simplified, but not by much.
Is this because it's re-instantiating the template each time I change the selected item, or something like that? Is there any way around that other than agonizingly maintaining every tiny bit of UI state in the ViewModel?
Update
I was right, this is the virtualization bug, it's a known bug, and the designer probably considered it a feature. He's insane, of course: A tab control is not a list box. It becomes totally unusable long before you've got enough items in it for virtualization to make any sense at all.
The answer seems to be Ivan Krivyakov's TabContent.IsCached attached property, found in this question: Turning off Virtualization for Tabcontrol without itemsource - WPF. It's not the answer to his question, but it is the answer to mine.
Looks like this in the XAML:
<TabControl
xmlns:ikriv="clr-namespace:IKriv.Windows.Controls.Behaviors"
ikriv:TabContent.IsCached="True"
ItemsSource="{Binding DocumentList.Documents}"
SelectedItem="{Binding DocumentList.ActiveDocument}"
/>
And here's the complete source code to Krivyakov's solution, just in case CodeProject gets hit by an asteroid:
// TabContent.cs, version 1.2
// The code in this file is Copyright (c) Ivan Krivyakov
// See http://www.ikriv.com/legal.php for more information
//
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
/// <summary>
/// http://www.codeproject.com/Articles/460989/WPF-TabControl-Turning-Off-Tab-Virtualization
/// </summary>
namespace IKriv.Windows.Controls.Behaviors
{
/// <summary>
/// Attached properties for persistent tab control
/// </summary>
/// <remarks>By default WPF TabControl bound to an ItemsSource destroys visual state of invisible tabs.
/// Set ikriv:TabContent.IsCached="True" to preserve visual state of each tab.
/// </remarks>
public static class TabContent
{
public static bool GetIsCached(DependencyObject obj)
{
return (bool)obj.GetValue(IsCachedProperty);
}
public static void SetIsCached(DependencyObject obj, bool value)
{
obj.SetValue(IsCachedProperty, value);
}
/// <summary>
/// Controls whether tab content is cached or not
/// </summary>
/// <remarks>When TabContent.IsCached is true, visual state of each tab is preserved (cached), even when the tab is hidden</remarks>
public static readonly DependencyProperty IsCachedProperty =
DependencyProperty.RegisterAttached("IsCached", typeof(bool), typeof(TabContent), new UIPropertyMetadata(false, OnIsCachedChanged));
public static DataTemplate GetTemplate(DependencyObject obj)
{
return (DataTemplate)obj.GetValue(TemplateProperty);
}
public static void SetTemplate(DependencyObject obj, DataTemplate value)
{
obj.SetValue(TemplateProperty, value);
}
/// <summary>
/// Used instead of TabControl.ContentTemplate for cached tabs
/// </summary>
public static readonly DependencyProperty TemplateProperty =
DependencyProperty.RegisterAttached("Template", typeof(DataTemplate), typeof(TabContent), new UIPropertyMetadata(null));
public static DataTemplateSelector GetTemplateSelector(DependencyObject obj)
{
return (DataTemplateSelector)obj.GetValue(TemplateSelectorProperty);
}
public static void SetTemplateSelector(DependencyObject obj, DataTemplateSelector value)
{
obj.SetValue(TemplateSelectorProperty, value);
}
/// <summary>
/// Used instead of TabControl.ContentTemplateSelector for cached tabs
/// </summary>
public static readonly DependencyProperty TemplateSelectorProperty =
DependencyProperty.RegisterAttached("TemplateSelector", typeof(DataTemplateSelector), typeof(TabContent), new UIPropertyMetadata(null));
[EditorBrowsable(EditorBrowsableState.Never)]
public static TabControl GetInternalTabControl(DependencyObject obj)
{
return (TabControl)obj.GetValue(InternalTabControlProperty);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetInternalTabControl(DependencyObject obj, TabControl value)
{
obj.SetValue(InternalTabControlProperty, value);
}
// Using a DependencyProperty as the backing store for InternalTabControl. This enables animation, styling, binding, etc...
[EditorBrowsable(EditorBrowsableState.Never)]
public static readonly DependencyProperty InternalTabControlProperty =
DependencyProperty.RegisterAttached("InternalTabControl", typeof(TabControl), typeof(TabContent), new UIPropertyMetadata(null, OnInternalTabControlChanged));
[EditorBrowsable(EditorBrowsableState.Never)]
public static ContentControl GetInternalCachedContent(DependencyObject obj)
{
return (ContentControl)obj.GetValue(InternalCachedContentProperty);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetInternalCachedContent(DependencyObject obj, ContentControl value)
{
obj.SetValue(InternalCachedContentProperty, value);
}
// Using a DependencyProperty as the backing store for InternalCachedContent. This enables animation, styling, binding, etc...
[EditorBrowsable(EditorBrowsableState.Never)]
public static readonly DependencyProperty InternalCachedContentProperty =
DependencyProperty.RegisterAttached("InternalCachedContent", typeof(ContentControl), typeof(TabContent), new UIPropertyMetadata(null));
[EditorBrowsable(EditorBrowsableState.Never)]
public static object GetInternalContentManager(DependencyObject obj)
{
return (object)obj.GetValue(InternalContentManagerProperty);
}
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetInternalContentManager(DependencyObject obj, object value)
{
obj.SetValue(InternalContentManagerProperty, value);
}
// Using a DependencyProperty as the backing store for InternalContentManager. This enables animation, styling, binding, etc...
public static readonly DependencyProperty InternalContentManagerProperty =
DependencyProperty.RegisterAttached("InternalContentManager", typeof(object), typeof(TabContent), new UIPropertyMetadata(null));
private static void OnIsCachedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj == null) return;
var tabControl = obj as TabControl;
if (tabControl == null)
{
throw new InvalidOperationException("Cannot set TabContent.IsCached on object of type " + args.NewValue.GetType().Name +
". Only objects of type TabControl can have TabContent.IsCached property.");
}
bool newValue = (bool)args.NewValue;
if (!newValue)
{
if (args.OldValue != null && ((bool)args.OldValue))
{
throw new NotImplementedException("Cannot change TabContent.IsCached from True to False. Turning tab caching off is not implemented");
}
return;
}
EnsureContentTemplateIsNull(tabControl);
tabControl.ContentTemplate = CreateContentTemplate();
EnsureContentTemplateIsNotModified(tabControl);
}
private static DataTemplate CreateContentTemplate()
{
const string xaml =
"<DataTemplate><Border b:TabContent.InternalTabControl=\"{Binding RelativeSource={RelativeSource AncestorType=TabControl}}\" /></DataTemplate>";
var context = new ParserContext();
context.XamlTypeMapper = new XamlTypeMapper(new string[0]);
context.XamlTypeMapper.AddMappingProcessingInstruction("b", typeof(TabContent).Namespace, typeof(TabContent).Assembly.FullName);
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("b", "b");
var template = (DataTemplate)XamlReader.Parse(xaml, context);
return template;
}
private static void OnInternalTabControlChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj == null) return;
var container = obj as Decorator;
if (container == null)
{
var message = "Cannot set TabContent.InternalTabControl on object of type " + obj.GetType().Name +
". Only controls that derive from Decorator, such as Border can have a TabContent.InternalTabControl.";
throw new InvalidOperationException(message);
}
if (args.NewValue == null) return;
if (!(args.NewValue is TabControl))
{
throw new InvalidOperationException("Value of TabContent.InternalTabControl cannot be of type " + args.NewValue.GetType().Name +", it must be of type TabControl");
}
var tabControl = (TabControl)args.NewValue;
var contentManager = GetContentManager(tabControl, container);
contentManager.UpdateSelectedTab();
}
private static ContentManager GetContentManager(TabControl tabControl, Decorator container)
{
var contentManager = (ContentManager)GetInternalContentManager(tabControl);
if (contentManager != null)
{
/*
* Content manager already exists for the tab control. This means that tab content template is applied
* again, and new instance of the Border control (container) has been created. The old container
* referenced by the content manager is no longer visible and needs to be replaced
*/
contentManager.ReplaceContainer(container);
}
else
{
// create content manager for the first time
contentManager = new ContentManager(tabControl, container);
SetInternalContentManager(tabControl, contentManager);
}
return contentManager;
}
private static void EnsureContentTemplateIsNull(TabControl tabControl)
{
if (tabControl.ContentTemplate != null)
{
throw new InvalidOperationException("TabControl.ContentTemplate value is not null. If TabContent.IsCached is True, use TabContent.Template instead of ContentTemplate");
}
}
private static void EnsureContentTemplateIsNotModified(TabControl tabControl)
{
var descriptor = DependencyPropertyDescriptor.FromProperty(TabControl.ContentTemplateProperty, typeof(TabControl));
descriptor.AddValueChanged(tabControl, (sender, args) =>
{
throw new InvalidOperationException("Cannot assign to TabControl.ContentTemplate when TabContent.IsCached is True. Use TabContent.Template instead");
});
}
public class ContentManager
{
TabControl _tabControl;
Decorator _border;
public ContentManager(TabControl tabControl, Decorator border)
{
_tabControl = tabControl;
_border = border;
_tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
}
public void ReplaceContainer(Decorator newBorder)
{
if (Object.ReferenceEquals(_border, newBorder)) return;
_border.Child = null; // detach any tab content that old border may hold
_border = newBorder;
}
public void UpdateSelectedTab()
{
_border.Child = GetCurrentContent();
}
private ContentControl GetCurrentContent()
{
var item = _tabControl.SelectedItem;
if (item == null) return null;
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
if (tabItem == null) return null;
var cachedContent = TabContent.GetInternalCachedContent(tabItem);
if (cachedContent == null)
{
cachedContent = new ContentControl
{
DataContext = item,
ContentTemplate = TabContent.GetTemplate(_tabControl),
ContentTemplateSelector = TabContent.GetTemplateSelector(_tabControl)
};
cachedContent.SetBinding(ContentControl.ContentProperty, new Binding());
TabContent.SetInternalCachedContent(tabItem, cachedContent);
}
return cachedContent;
}
}
}
}
The carret position gets updated every time the text gets re-initialized in your case. You could put a behavior on the text box to set it to the end on focus... see the following: Set the caret/cursor position to the end of the string value WPF textbox

xaml Convert ID to String without using ComboBox

I've been using code like this
<ComboBox ItemsSource="{Binding Path=CompaniesViewModel.CompaniesCollection}"
SelectedValuePath="CompanyId"
SelectedValue="{Binding Path=CompanyId}"
IsEnabled="False"
DisplayMemberPath="CompanyName"
/>
To display a Company Name in a ComboBox. Notice how the IsEnabled is set to false...that's because I really don't want the user to use the ComboBox. I am just using it as an easy way to convert an ID to string for display purposes.
When I put items in a Grid and there are a lot of them, I think it is really hurting the rendering performance. When I remove the ComboBox it loads in a split second. When the ComboBox is used in the code it can take 20 seconds.
I guess my question is I think I should be using a Label or TextBlock but not sure how to get the binding to work correctly as They don't have an ItemsSource or a SelectedValuePath or SelectedValue.
I thought about writing an IValueConverter but not sure how to bind/pass in the 3 values. I'd have to pass in the collection, the ValuePath and the Value ID.
Any thoughts or suggestions?
Put a
public Company Company {get {return CompaniesCollection.FirstOrDefault(x => x.CompanyId == CompanyId); }}
property in the ViewModel.
I welcome any of you to examine my code to see if you can make it more efficient but this is what I ended up doing.
<cc:LookupLabel
ItemsSource="{Binding Path=CompaniesCollection}"
SelectedValuePath="CompanyId"
SelectedValue="{Binding Path=CompanyId}"
DisplayMemberPath="CompanyName"
/>
And below is the LookupLabel derived from Label and INotifyPropertyChanged. I'm not sure how Microsoft implements this efficiently but this was my best stab at it. In particular the GetContent method listed at the bottom. All the other stuff is just the messy DependencyProperty declarations.
using System;
using System.Collections;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace CustomControls
{
public class LookupLabel : Label, INotifyPropertyChanged
{
public LookupLabel()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#region ItemsSource
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(LookupLabel)
, new UIPropertyMetadata(null, LookupLabel.ItemsSourceChanged)
);
private static void ItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
LookupLabel t = d as LookupLabel;
t.NotifyPropertyChanged("ItemsSource");
t.Content = GetContent(t);
}
[Bindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IEnumerable ItemsSource
{
get
{
return (IEnumerable)GetValue(ItemsSourceProperty);
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
#endregion ItemsSource
#region SelectedValue
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(LookupLabel)
, new UIPropertyMetadata(null, LookupLabel.SelectedValueChanged)
);
private static void SelectedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
LookupLabel t = d as LookupLabel;
t.NotifyPropertyChanged("SelectedValue");
t.Content = GetContent(t);
}
[Localizability(LocalizationCategory.NeverLocalize)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Bindable(true)]
[Category("Appearance")]
public object SelectedValue
{
get
{
return (object)GetValue(SelectedValueProperty);
}
set
{
SetValue(SelectedValueProperty, value);
}
}
#endregion SelectedValue
#region SelectedValuePath
public static readonly DependencyProperty SelectedValuePathProperty =
DependencyProperty.Register("SelectedValuePath", typeof(string), typeof(LookupLabel)
, new UIPropertyMetadata(string.Empty, LookupLabel.SelectedValuePathChanged)
);
private static void SelectedValuePathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
LookupLabel t = d as LookupLabel;
t.NotifyPropertyChanged("SelectedValuePath");
t.Content = GetContent(t);
}
[Localizability(LocalizationCategory.NeverLocalize)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Bindable(true)]
[Category("Appearance")]
public string SelectedValuePath
{
get
{
return (string)GetValue(SelectedValuePathProperty);
}
set
{
SetValue(SelectedValuePathProperty, value);
}
}
#endregion SelectedValuePath
#region DisplayMemberPath
public static readonly DependencyProperty DisplayMemberPathProperty =
DependencyProperty.Register("DisplayMemberPath", typeof(string), typeof(LookupLabel)
, new UIPropertyMetadata(string.Empty, LookupLabel.DisplayMemberPathChanged)
);
private static void DisplayMemberPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
LookupLabel t = d as LookupLabel;
t.NotifyPropertyChanged("DisplayMemberPath");
t.Content = GetContent(t);
}
[Localizability(LocalizationCategory.NeverLocalize)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Bindable(true)]
[Category("Appearance")]
public string DisplayMemberPath
{
get
{
return (string)GetValue(DisplayMemberPathProperty);
}
set
{
SetValue(DisplayMemberPathProperty, value);
}
}
#endregion DisplayMemberPath
protected static object GetContent(LookupLabel label)
{
if (label.ItemsSource == null)
{
return null;
}
if (string.IsNullOrWhiteSpace(label.SelectedValuePath))
{
return null;
}
if (string.IsNullOrWhiteSpace(label.DisplayMemberPath))
{
return null;
}
if (label.SelectedValue == null)
{
return null;
}
object result = null;
System.Reflection.PropertyInfo valuePropertyInfo = null;
foreach (var item in label.ItemsSource)
{
if (valuePropertyInfo == null)
{
valuePropertyInfo = item.GetType().GetProperty(label.SelectedValuePath);
if (valuePropertyInfo == null)
{
return null;
}
}
if (valuePropertyInfo.GetValue(item, null).Equals(label.SelectedValue))
{
var displayPropertInfo = item.GetType().GetProperty(label.DisplayMemberPath);
if (displayPropertInfo == null)
{
return null;
}
else
{
result = displayPropertInfo.GetValue(item, null);
break;
}
}
}
return result;
}
}
}
I am guessing that your combobox is taking too long to load because your collection has a lot of items.
You shouldn't be loading all your companies if you'll just show one of them, as a general good-practice.
I don't quite grasp your intent in using the combobox. Is it the style? can it be enabled in the future?
If it is only an easy way of displaying the CompanyName then i'd suggest the following:
Bind directly to CompanyName property.
-OR-
in the code-behind, create a calculated property named "CompanyDisplayName" that gets your company name.
Bind to it in the XAML
in the code-behind, whenever the current selected Company instance or the CompanyId changes fire 'OnPropertyChanged("CompanyDisplayName")
Try a TextBlock or a readonly TextBox to enable copy/paste;
For more info on the NotifyPropertyCahnged paradigm read here

WPF Datagrid: MVVM friendly way to bind SelectedCells to my ViewModel

I'm using the WPF datagrid, and have SelectionUnit="Cell" and SelectionMode="Extended". I'm also trying to adhere to the MVVM principals as much as I can.
I need my ViewModel to keep track of the current SelectedCells.
Life would be easy if I could just Bind its SelectedCells property to my ViewModel. Oddly enough, SelectedCells is only raised once - when we first select any cell in the grid.
MS explains it here: http://social.msdn.microsoft.com/Forums/en/wpf/thread/737117f4-6d20-4232-88cf-e52cc44d4431
Can anyone think of an MVVM-friendly approach to get around it?
Thanks!
I realized my last answer was for SelectedItems instead of SelectedCells, so I wrote a complete attached property class to do data binding for multiple SelectedCells which works as follows:
<controls:DataGrid ItemsSource="{StaticResource list}"
SelectionMode="Extended"
behaviors:DataGridSelectedCellsBehavior.SelectedCells="{Binding Path=SelectedGridCellCollection, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
I have a working source code and a demo project of it here.
Attached property behavior Code :
public class DataGridSelectedCellsBehavior
{
// Source : https://archive.codeplex.com/?p=datagridthemesfromsl
// Credit to : T. Webster, https://stackoverflow.com/users/266457/t-webster
public static IList<DataGridCellInfo> GetSelectedCells(DependencyObject obj)
{
return (IList<DataGridCellInfo>)obj.GetValue(SelectedCellsProperty);
}
public static void SetSelectedCells(DependencyObject obj, IList<DataGridCellInfo> value)
{
obj.SetValue(SelectedCellsProperty, value);
}
public static readonly DependencyProperty SelectedCellsProperty = DependencyProperty.RegisterAttached("SelectedCells", typeof(IList<DataGridCellInfo>), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null, OnSelectedCellsChanged));
static SelectedCellsChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
{
return (SelectedCellsChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
}
static void SetSelectionChangedHandler(DependencyObject obj, SelectedCellsChangedEventHandler value)
{
obj.SetValue(SelectionChangedHandlerProperty, value);
}
static readonly DependencyProperty SelectionChangedHandlerProperty = DependencyProperty.RegisterAttached("SelectedCellsChangedEventHandler", typeof(SelectedCellsChangedEventHandler), typeof(DataGridSelectedCellsBehavior), new UIPropertyMetadata(null));
//d is MultiSelector (d as ListBox not supported)
static void OnSelectedCellsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (GetSelectionChangedHandler(d) != null)
return;
if (d is DataGrid)//DataGrid
{
DataGrid datagrid = d as DataGrid;
SelectedCellsChangedEventHandler selectionchanged = null;
foreach (var selected in GetSelectedCells(d) as IList<DataGridCellInfo>)
datagrid.SelectedCells.Add(selected);
selectionchanged = (sender, e) =>
{
SetSelectedCells(d, datagrid.SelectedCells);
};
SetSelectionChangedHandler(d, selectionchanged);
datagrid.SelectedCellsChanged += GetSelectionChangedHandler(d);
}
//else if (d is ListBox)
//{
// ListBox listbox = d as ListBox;
// SelectionChangedEventHandler selectionchanged = null;
// selectionchanged = (sender, e) =>
// {
// SetSelectedCells(d, listbox.SelectedCells);
// };
// SetSelectionChangedHandler(d, selectionchanged);
// listbox.SelectionChanged += GetSelectionChangedHandler(d);
//}
}
}
View Model Code :
class DemoViewModel : INotifyPropertyChanged
{
private IList<DataGridCellInfo> selectedGridCellCollection = new List<DataGridCellInfo>();
public IList<DataGridCellInfo> SelectedGridCellCollection
{
get { return selectedGridCellCollection; }
set
{
selectedGridCellCollection = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Do you need the SelectedCells constantly data-binded, or just when the user hits the OK/Accept button? If you only need it at the end of whatever process the user is in you can bind the SelectedCells to the CommandParameter property of a Button, for example. The SelectedCells is an IList, and you know enough to just do a cast to whatever object type the selection actually is. The other option is messier, you can use an attached property, keeping the event-handling out of your Views. This attached property would handle either a ListBox or in your case a DataGrid (MultiSelector).
public class Attach
{
public static IList GetSelectedItems(DependencyObject obj)
{
return (IList)obj.GetValue(SelectedItemsProperty);
}
public static void SetSelectedItems(DependencyObject obj, IList value)
{
obj.SetValue(SelectedItemsProperty, value);
}
/// <summary>
/// Attach this property to expose the read-only SelectedItems property of a MultiSelector for data binding.
/// </summary>
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.RegisterAttached("SelectedItems", typeof(IList), typeof(Attach), new UIPropertyMetadata(new List<object>() as IList, OnSelectedItemsChanged));
static SelectionChangedEventHandler GetSelectionChangedHandler(DependencyObject obj)
{
return (SelectionChangedEventHandler)obj.GetValue(SelectionChangedHandlerProperty);
}
static void SetSelectionChangedHandler(DependencyObject obj, SelectionChangedEventHandler value)
{
obj.SetValue(SelectionChangedHandlerProperty, value);
}
static readonly DependencyProperty SelectionChangedHandlerProperty =
DependencyProperty.RegisterAttached("SelectionChangedHandler", typeof(SelectionChangedEventHandler), typeof(Attach), new UIPropertyMetadata(null));
//d is MultiSelector (d as ListBox not supported)
static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
if (GetSelectionChangedHandler(d) != null)
return;
if (d is MultiSelector)//DataGrid
{
MultiSelector multiselector = d as MultiSelector;
SelectionChangedEventHandler selectionchanged = null;
foreach (var selected in GetSelectedItems(d) as IList)
multiselector.SelectedItems.Add(selected);
selectionchanged = (sender, e) =>
{
SetSelectedItems(d, multiselector.SelectedItems);
};
SetSelectionChangedHandler(d, selectionchanged);
multiselector.SelectionChanged += GetSelectionChangedHandler(d);
}
else if (d is ListBox)
{
ListBox listbox = d as ListBox;
SelectionChangedEventHandler selectionchanged = null;
selectionchanged = (sender, e) =>
{
SetSelectedItems(d, listbox.SelectedItems);
};
SetSelectionChangedHandler(d, selectionchanged);
listbox.SelectionChanged += GetSelectionChangedHandler(d);
}}}
Usage in XAML:
<DataGrid ItemsSource="{Binding Path=SourceList}"
myControls:Attach.SelectedItems="{Binding Path=myMvvmSelectedItems, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended" />
You might be interested in the BookLibrary sample application of the WPF Application Framework (WAF). It shows how to synchronize the DataGrid.SelectedItems with the ViewModel. This might be very similar to SelectedCells.
Somewhere between perfect MVVM bindings and full event handler codebehinds there is the gray area of interactivity EventTriggers (see Blend SDK) :)
If you put an eventtrigger to the datagrid, and set to "SelectionChanged" and pass the eventargs to a command (use an EventToCommand actiontrigger) you could get the selected items from the eventargs hopefully...
Or use the multibinding as said in the MS thread :)

WPF Binding Not Working

I am pretty sure I am doing something dreadfully wrong, but can't figure it out.
I created a simple wrapper around a class and added a dependency property so I could bind to it. However, the binding gives no errors, but does nothing.
In order to simplify things I changed the class to TextBox, and got the same results.
public class TextEditor : TextBox
{
#region Public Properties
#region EditorText
/// <summary>
/// Gets or sets the text of the editor
/// </summary>
public string EditorText
{
get
{
return (string)GetValue(EditorTextProperty);
}
set
{
//if (ValidateEditorText(value) == false) return;
if (EditorText != value)
{
SetValue(EditorTextProperty, value);
base.Text = value;
//if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("EditorText"));
}
}
}
public static readonly DependencyProperty EditorTextProperty =
DependencyProperty.Register("EditorText", typeof(string), typeof(TextEditor));
#endregion
#endregion
#region Constructors
public TextEditor()
{
//Attach to the text changed event
//TextChanged += new EventHandler(TextEditor_TextChanged);
}
#endregion
#region Event Handlers
private void TextEditor_TextChanged(object sender, EventArgs e)
{
EditorText = base.Text;
}
#endregion
}
When I run the following XAML the first gives results, but the second one (EditorText) doesn't even hit the EditorText property.
<local:TextEditor IsReadOnly="True" Text="{Binding Path=RuleValue, Mode=TwoWay}" WordWrap="True" />
<local:TextEditor IsReadOnly="True" EditorText="{Binding Path=RuleValue, Mode=TwoWay}" WordWrap="True" />
You're doing extra work in your CLR property. There is no guarantee that your CLR property will be used by WPF so you shouldn't be doing this. Instead, use metadata on your DP to achieve the same effect.
public string EditorText
{
get { return (string)GetValue(EditorTextProperty); }
set { SetValue(EditorTextProperty, value); }
}
public static readonly DependencyProperty EditorTextProperty =
DependencyProperty.Register(
"EditorText",
typeof(string),
typeof(TextEditor),
new FrameworkPropertyMetadata(OnEditorTextChanged));
private static void OnEditorTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
var textEditor = dependencyObject as TextEditor;
// do your extraneous work here
}

Resources