I have a ListBox of Items and a Search TextBox and Search Button, i want to enter the search text in the TextBox and Click Search Button so the ListBox highlight that item and get it on screen (for lengthy list).
Is it possible to do this using ICollectionView? and if not possible how to implement this scenario.
Note: after googling i found all samples talking about Filtering but i need searching.
Thanks for bearing with us.
You can achieve this by implementing a Prism Behavior:
public class AutoScrollingBehavior:Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
var itemsSource = AssociatedObject.ItemsSource as ICollectionView;
if (itemsSource == null)
return;
itemsSource.CurrentChanged += ItemsSourceCurrentChanged;
}
void ItemsSourceCurrentChanged(object sender, EventArgs e)
{
AssociatedObject.ScrollIntoView(((ICollectionView)sender).CurrentItem);
AssociatedObject.Focus();
}
}
Another approach is listening to ListBox.SelectionChanged instead of ICollectionView.CurrentChanged.
public class AutoScrollingBehavior:Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged;
}
void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count <= 0)
return;
AssociatedObject.ScrollIntoView(e.AddedItems[0]);
AssociatedObject.Focus();
}
}
On Xaml:
<ScrollViewer Height="200">
<ListBox x:Name="listbox" ItemsSource="{Binding Path=NamesView}" SelectionMode="Single"
IsSynchronizedWithCurrentItem="True">
<i:Interaction.Behaviors>
<local:AutoScrollingBehavior/>
</i:Interaction.Behaviors>
</ListBox>
</ScrollViewer>
Inside searching command, you set NamesView.MoveCurrentTo(foundItem). However this approach will only scroll to the edge, instead of center, might you expected. If you want it to scroll to the center, you might need ItemContainerGenerator.
In your view model who holds the ICollectionView:
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
RaisePropertyChanged("SearchText");
}
}
private ICommand _searchCommand;
public ICommand SearchCommand
{
get { return _searchCommand ?? (_searchCommand = new DelegateCommand(Search)); }
}
private void Search()
{
var item = _names.FirstOrDefault(name => name == SearchText);
if (item == null) return;
NamesView.MoveCurrentTo(item);
}
On Xaml, bind TextBox.Text to SearchText and bind search button's Command to SearchCommand.
Hope it can help.
Related
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.
I've got my custom ComboBox:
public class CustomComboBox : ComboBox
{
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
// allow to go into items using up and down arrows without causing the text change
e.Handled = true;
}
}
and xaml as:
<Window>
<Grid>
<wpfApplication1:CustomComboBox IsEditable="True"
Width="200"
Height="25"
IsTextSearchEnabled="False"
StaysOpenOnEdit="True"
x:Name="cb"
PreviewTextInput="Cb_OnPreviewTextInput"
ItemsSource="{Binding Projects}"
Text="{Binding Text}"
SelectionChanged="Cb_OnSelectionChanged">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</wpfApplication1:CustomComboBox>
</Grid>
</Window>
Code of the window:
public partial class MainWindow : INotifyPropertyChanged
{
public ICollectionView Projects { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
ObservableCollection = new ObservableCollection<string>(Enumerable.Range(1, 1000).Select(i => $"Item {i}"));
Projects = CollectionViewSource.GetDefaultView(ObservableCollection);
}
private string _text;
public ObservableCollection<string> ObservableCollection { get; set; }
public string Text
{
get { return _text; }
set
{
if (_text != value)
{
_text = value;
OnPropertyChanged();
Projects.Filter = f =>
{
var search = Text.ToLower();
var item = f.ToString().ToLower();
return item.Contains(search);
};
}
}
}
private void Cb_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
cb.IsDropDownOpen = true;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
When Cb_OnPreviewTextInput is called I set IsDropdownOpen = true. In the first attempt (after typing the first letter) the first item on the list is selected and I can go up and down using relevant arrows, the caret is still in the TextBox.
When I keep on typing at that point, I',m not able to navigate up and down (1 item at time) anymore, at this point the whole ScrollViewer gets focus and I can only go to the bottom or to the top, but not 1 by 1. I have to close the popup e.g. by pressing Escape and then reopen by typing 1 character to be able to go up/down again.
I also noticed that after pressing PageUp the first item gets selected as well, so I tried to mimic that in code, but no luck.
Anyone knows what to do here to be able to navigate up/down and type without problems?
I tried things like:
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (_popup != null && e.Key == Key.Up || e.Key == Key.Down)
{
Dispatcher.BeginInvoke(DispatcherPriority.Input,
new Action(() =>
{
var c = _popup.Child;
var vs = c.GetChildOfType<VirtualizingStackPanel>();
vs.PageUp();
vs.Focus();
Keyboard.Focus(vs);
}));
}
base.OnPreviewKeyDown(e);
}
But that didn't work.
Ended up with writing a new template with a ListView in it.
Plus custom logic inside OnPreviewKeyDown :
if (e.Key == Key.Up || e.Key == Key.Down)
{
listView.Focus();
listView.Items.MoveCurrentTo(listView.SelectedItem);
if (e.Key == Key.Up)
listView.Items.MoveCurrentToPrevious();
else
listView.Items.MoveCurrentToNext();
listView.SelectedItem = listView.Items.CurrentItem;
listView.ScrollIntoView(listView.Items.CurrentItem);
e.Handled = true;
return;
}
Working well ;)
I have a listview which property SelectionMode is set to Extended.
Also I have a trash button. I am trying to perform delete operation on selected items on listview when user drags one or multiple selected items (using shift key if more than one item) and drop over delete button.
I have implemented it with a single selected item in listview and it works. Now I am trying to do the same with multiple selected items in listview without success.
Below code works when one single item is selected in listview and user drag and drop it from listview to delete button:
<ListView Margin="10" Name="lvUsers" ItemsSource="{Binding Path=Items}" SelectionMode="Extended">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="MouseMove" Handler="lstItems_MouseMove" />
</Style>
</ListView.ItemContainerStyle>
<!-- other stuff -->
</Listview>
<Button AllowDrop="True" Drop="btnDelete_Drop" Height="64" Width="64" Margin="10" Click="BtnDelete_Click" Content="Delete"/>
and the code-behind:
private void lstItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source != null)
{
DataModel selectedItem = (DataModel)lvUsers.SelectedItem;
DragDrop.DoDragDrop(lvUsers, selectedItem, DragDropEffects.Move);
}
}
}
private void btnDelete_Drop(object sender, DragEventArgs e)
{
Type myType = typeof(DataModel);
string modelns = myType.FullName;
DataModel selectedItem = e.Data.GetData(modelns) as DataModel;
MessageBox.Show(selectedItem.Name);
}
Each listviewitem on listview is of data type below:
public class DataModel
{
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public string Mail
{
get;
set;
}
}
As an example, when user drops the dragged listviewitem over delete button I show a message box with the name of the person.
How can I do the same but instead of dragging and dropping one single item from listview, do the same for multiple selected listviewitems? Once dropped, within
btnDelete_Drop I want to iterate over all the listviews items dropped and do some stuff.
I have just implemented a solution that works. I have replaced methods lstItem_MouseMove and btnDelete_Drop in code-behind by these ones:
private void lstItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source != null)
{
List<DataModel> myList = new List<DataModel>();
foreach (DataModel Item in lvUsers.SelectedItems)
{
myList.Add(Item);
}
DataObject dataObject = new DataObject(myList);
DragDrop.DoDragDrop(lvUsers, dataObject, DragDropEffects.Move);
}
}
}
private void btnDelete_Drop(object sender, DragEventArgs e)
{
Type myType = typeof(List<DataModel>);
List<DataModel> selectedItems = e.Data.GetData(myType) as List<DataModel>;
string hello = "Hello ";
foreach (DataModel dm in selectedItems)
{
hello = hello + ", " + dm.Name;
}
MessageBox.Show(hello);
}
This is working but mayber there are any other better solution. Any suggestion will be welcome.
I am building a panorama which displays images through binding. I need to find index of panorama item whenever the current item changes. But the SELECTIONCHANGED event is not firing in case when data is retrieved through binding. Can you please suggest some other way. Thanx in advance
XAML Code
<phone:Panorama x:Name="HeaderPanorama"
ItemsSource="{Binding PanoramaImages}"
Width="550" Margin="-10,-255,0,-140"
SelectionChanged="HeaderPanorama_SelectionChanged_1">
<phone:Panorama.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Margin="-10"/>
</DataTemplate>
</phone:Panorama.ItemTemplate>
</phone:Panorama>
CodeBehind
private void HeaderPanorama_SelectionChanged_1(
object sender,
SelectionChangedEventArgs e)
{
if (this.DataContext != null && this.DataContext is HomeViewModel)
{
((HomeViewModel)this.DataContext).PanoramaItemIndex =
HeaderPanorama.SelectedIndex;
}
}
ViewModel Code
public HomeViewModel()
{
RequestHomeData();
PanoramaImages = new List<string>();
PanoramaImages.Add("/Assets/n.png");
PanoramaImages.Add("/Assets/n.png");
PanoramaImages.Add("/Assets/n.png");
PanoramaImages.Add("/Assets/n.png");
}
private List<string> _panoramaImages;
public List<string> PanoramaImages
{
get { return _panoramaImages; }
set
{
_panoramaImages = value;
NotifyPropertyChanged("PanoramaImages");
}
}
private int _panoramaItemIndex;
public int PanoramaItemIndex
{
get { return _panoramaItemIndex; }
set
{
_panoramaItemIndex = value;
NotifyPropertyChanged("PanoramaItemIndex");
}
}
Scenario: In a Silverlight 4 MVVM project, we have a ListBox control containing items, the selected item is two-way-bound to the appropriate property in the ViewModel. Another control (for example reasons, I've stripped it down to a single TextBox) is data bound to the selected item's content. The value should update on leave/focus lost.
Problem: When the value in the TextBox is changed and we leave that TextBox by pressing the Tab key, everything works as desired - the value is updated. However, if the user clicks on a different item in the ListBox, then the SelectedItem setter is fired before the content of TextBox setter is fired, leaving no chance to handle the user input.
You can see in debugger, when adding breakpoints to the property setters, that the new ListView selection is applied first, before the TextBox update is processed.
Desired behavior: We need to know that the currently selected item was modified before the user has selected another item. It's not desired to have a custom update trigger which would notify on each key press (we know that's possible).
Can you help?
Code (a very simple example):
ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ItemViewModel : ViewModelBase
{
private string _content;
public ItemViewModel(string initContent)
{
_content = initContent;
}
public string Content
{
get
{
return _content;
}
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
}
public ItemViewModel SelectedItem
{
get
{
return _selectedViewModel;
}
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
XAML
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Height="100"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
DisplayMemberPath="Content"
Width="220" />
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="12,118,0,0"
Text="{Binding SelectedItem.Content, Mode=TwoWay}"
VerticalAlignment="Top"
Width="220" />
</Grid>
XAML Code Behind
public MvvmTestView()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
}
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
MainViewModel viewModel = new MainViewModel();
viewModel.Items.Add(new ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new ItemViewModel("Thanks to Community"));
DataContext = viewModel;
}
UPDATE 1
I present a self designed solution for you to check out, which will be probably be the accepted one, I still want to encourage you to make comments and give your hints. Thanks.
You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.
Here´s the code for the Behavior class:
public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
// Fields
private BindingExpression expression;
// Methods
protected override void OnAttached() {
base.OnAttached();
this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
base.AssociatedObject.TextChanged+= OnTextChanged;
}
protected override void OnDetaching() {
base.OnDetaching();
base.AssociatedObject.TextChanged-= OnTextChanged;
this.expression = null;
}
private void OnTextChanged(object sender, EventArgs args) {
this.expression.UpdateSource();
}
}
Heres the XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"
<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox >
This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).
How to get there?
In ViewModelBase, implement a new ForceBindingUpdate event:
public abstract class ViewModelBase : INotifyPropertyChanged
{
// ----- leave everything from original code ------
public event EventHandler ForceBindingUpdate;
protected void OnForceBindingUpdate()
{
var handler = ForceBindingUpdate;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
In MainViewModel, update the setter of the SelectedItem property:
set // of SelectedItem Property
{
if (_selectedViewModel != value)
{
// Ensure Data Update - the new part
OnForceBindingUpdate();
// Old stuff
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
Update the MvvmTestView Code Behind to implement the new event:
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
// remains unchanged
Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));
// Ensure Data Update by rebinding the content property - the new part
viewModel.ForceBindingUpdate += (s, a) =>
{
var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();
};
// remains unchanged
DataContext = viewModel;
}
Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.
Done.
Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.
Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?
Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.
Solution №1
public class LazyTextBox: TextBox
{
//bind to that property instead..
public string LazyText
{
get { return (string)GetValue(LazyTextProperty); }
set { SetValue(LazyTextProperty, value); }
}
public static readonly DependencyProperty LazyTextProperty =
DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox),
new PropertyMetadata(null));
//call this method when it's really nessasary...
public void EnsureThatLazyTextEqualText()
{
if (this.Text != this.LazyText)
{
this.LazyText = this.Text;
}
}
}
Solution №2 (works as magic :) )
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items { get { return _items; } }
public ItemViewModel SelectedItem
{
get { return _selectedViewModel; }
set
{
if (_selectedViewModel != value)
{
if (SelectedItem != null)
{
SelectedItem.Content = SelectedItem.Content;
}
_selectedViewModel = value;
// A little delay make no harm :)
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0.1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
}
}
void t_Tick(object sender, EventArgs e)
{
OnPropertyChanged("SelectedItem");
(sender as DispatcherTimer).Stop();
}
}
I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.
By putting a ghost element to take focus we can swap the focus back in forth forcing
the text box to commit its contents. So in the code behind we take care of the focus wiggle.
But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.
<MenuItem Header="_Save"
Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
UIElement elem = Keyboard.FocusedElement as UIElement;
Keyboard.Focus(ghost);
Keyboard.Focus(elem);
}
Solution #3
public abstract class ViewModelBase : INotifyPropertyChanged
{
private List<string> _propNameList = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
_propNameList.Add(propertyName);
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
void t_Tick(object sender, EventArgs e)
{
if (_propNameList.Count > 0)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(_propNameList[0]));
_propNameList.Remove(_propNameList[0]);
}
}
}
PS: it's the same timer.. but this solution is more generic..