I currently have a requirement to notify my application user if any fields have been changed/updated on a View.
For example, if the user changes a date field on the View and then tries to close the View, the application would display a message asking the user to Continue and lose changes or Cancel so that they can click the Save button.
Problem is: How do I detect that any of the data fields changed in the View?
Hope this makes sense, than you in advance, regards,
One approach you can take is to leverage the IChangeTracking and INotifyPropertyChanged interfaces.
If you create an abstract base class that your view models inherit from (ViewModelBase) which implements the IChangeTracking and INotifyPropertyChanged interfaces, you can have your view model base attach to notification of property changes (in effect signaling that the view model has been modified) and which will set the IsChanged property to true to indicate that the view model is 'dirty'.
Using this approach, you are relying on property change notification via data binding to track changes and would reset the change tracking after any commits are made.
In the case you described you could handle the Unloaded or Closing event of your view to inspect the DataContext; and if the DataContext implements IChangeTracking you can use the IsChanged property to determine if any unaccepted changes have been made.
Simple example:
/// <summary>
/// Provides a base class for objects that support property change notification
/// and querying for changes and resetting of the changed status.
/// </summary>
public abstract class ViewModelBase : IChangeTracking, INotifyPropertyChanged
{
//========================================================
// Constructors
//========================================================
#region ViewModelBase()
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
/// </summary>
protected ViewModelBase()
{
this.PropertyChanged += new PropertyChangedEventHandler(OnNotifiedOfPropertyChanged);
}
#endregion
//========================================================
// Private Methods
//========================================================
#region OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
/// <summary>
/// Handles the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for this object.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param>
private void OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e != null && !String.Equals(e.PropertyName, "IsChanged", StringComparison.Ordinal))
{
this.IsChanged = true;
}
}
#endregion
//========================================================
// IChangeTracking Implementation
//========================================================
#region IsChanged
/// <summary>
/// Gets the object's changed status.
/// </summary>
/// <value>
/// <see langword="true"/> if the object’s content has changed since the last call to <see cref="AcceptChanges()"/>; otherwise, <see langword="false"/>.
/// The initial value is <see langword="false"/>.
/// </value>
public bool IsChanged
{
get
{
lock (_notifyingObjectIsChangedSyncRoot)
{
return _notifyingObjectIsChanged;
}
}
protected set
{
lock (_notifyingObjectIsChangedSyncRoot)
{
if (!Boolean.Equals(_notifyingObjectIsChanged, value))
{
_notifyingObjectIsChanged = value;
this.OnPropertyChanged("IsChanged");
}
}
}
}
private bool _notifyingObjectIsChanged;
private readonly object _notifyingObjectIsChangedSyncRoot = new Object();
#endregion
#region AcceptChanges()
/// <summary>
/// Resets the object’s state to unchanged by accepting the modifications.
/// </summary>
public void AcceptChanges()
{
this.IsChanged = false;
}
#endregion
//========================================================
// INotifyPropertyChanged Implementation
//========================================================
#region PropertyChanged
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region OnPropertyChanged(PropertyChangedEventArgs e)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event.
/// </summary>
/// <param name="e">A <see cref="PropertyChangedEventArgs"/> that provides data for the event.</param>
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
#endregion
#region OnPropertyChanged(string propertyName)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyName"/>.
/// </summary>
/// <param name="propertyName">The <see cref="MemberInfo.Name"/> of the property whose value has changed.</param>
protected void OnPropertyChanged(string propertyName)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
#endregion
#region OnPropertyChanged(params string[] propertyNames)
/// <summary>
/// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyNames"/>.
/// </summary>
/// <param name="propertyNames">An <see cref="Array"/> of <see cref="String"/> objects that contains the names of the properties whose values have changed.</param>
/// <exception cref="ArgumentNullException">The <paramref name="propertyNames"/> is a <see langword="null"/> reference (Nothing in Visual Basic).</exception>
protected void OnPropertyChanged(params string[] propertyNames)
{
if (propertyNames == null)
{
throw new ArgumentNullException("propertyNames");
}
foreach (var propertyName in propertyNames)
{
this.OnPropertyChanged(propertyName);
}
}
#endregion
}
In MVVM a View is binded to a View-Model which in turn is binded to a Model.
The view can not be dirty, since it's changes are reflected immediately to the View-Model.
If you want changes to be applied to Model only on "OK" or "Accept",
bind View to a View-Model that doesn't apply changes to Model,
until an ApplyCommand or AcceptCommand (that you define and implement) is executed.
(The commands that the View is binded to are implemented by the View-Model.)
Example - VM:
public class MyVM : INotifyPropertyChanged
{
public string MyText
{
get
{
return _MyText;
}
set
{
if (value == _MyText)
return;
_MyText = value;
NotifyPropertyChanged("MyText");
}
}
private string _MyText;
public string MyTextTemp
{
get
{
return _MyTextTemp;
}
set
{
if (value == _MyTextTemp)
return;
_MyTextTemp = value;
NotifyPropertyChanged("MyTextTemp");
NotifyPropertyChanged("IsTextDirty");
}
}
private string _MyTextTemp;
public bool IsTextDirty
{
get
{
return MyText != MyTextTemp;
}
}
public bool IsMyTextBeingEdited
{
get
{
return _IsMyTextBeingEdited;
}
set
{
if (value == _IsMyTextBeingEdited)
return;
_IsMyTextBeingEdited = value;
if (!value)
{
MyText = MyTextTemp;
}
NotifyPropertyChanged("IsMyTextBeingEdited");
}
}
private bool _IsMyTextBeingEdited;
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Example - View:
<Label Content="{Binding MyText}" />
<!-- You can translate the events to commands by using a suitable framework -->
<!-- or use code behind to update a new dependency property as in this example -->
<TextBox
LostFocus="TextBox_LostFocus"
GotFocus="TextBox_GotFocus"
Text="{Binding Path=MyTextTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
/>
Example - view - code behind:
public MainWindow()
{
InitializeComponent();
SetBinding(IsTextBoxFocusedProperty,
new Binding
{
Path = new PropertyPath("IsMyTextBeingEdited"),
Mode = BindingMode.OneWayToSource,
});
}
private void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
IsTextBoxFocused = false;
}
private void TextBox_GotFocus(object sender, RoutedEventArgs e)
{
IsTextBoxFocused = true;
}
#region IsTextBoxFocused
/// <summary>
/// Gets or Sets IsTextBoxFocused
/// </summary>
public bool IsTextBoxFocused
{
get
{
return (bool)this.GetValue(IsTextBoxFocusedProperty);
}
set
{
this.SetValue(IsTextBoxFocusedProperty, value);
}
}
/// <summary>
/// The backing DependencyProperty behind IsTextBoxFocused
/// </summary>
public static readonly DependencyProperty IsTextBoxFocusedProperty = DependencyProperty.Register(
"IsTextBoxFocused", typeof(bool), typeof(MainWindow), new PropertyMetadata(default(bool)));
#endregion
Idea: check entitystate:
Problem is that this refers to the whole VIEW, so when a new participant (refreshes form) is selected before any editing, the value is also "Modified". After a save, if nothing else changes and we don’t switch participants, the value is "Unchanged"
Related
I've set up the binding of a ListView following this example, binding to observable collection but when I run the application the collection values aren't displayed in the ListView.
The output window isn't throwing any binding errors, so not sure what the binding error could be.
Also I've set a breakpoint on the list before its sent to the second VM and it's populated, ie, not null.
My guess is that the list is null in the second VM as it's not being initialized properly after being passed over.
Can anyone advise how to debug the ListView being empty?
This is the binding set in the View:
<ListBox ItemsSource="{Binding AddedSubjectGradePairsCopy}" Height="400" Margin="0,0,0,-329" VerticalAlignment="Top">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Subject}" /><Run Text=" - " /><Run Text="{Binding Points}" />
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The data context for the View is set as follows in the code behind:
namespace LC_Points.View
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ViewSubjectGradePage : Page
{
private NavigationHelper navigationHelper;
private ViewSubjectGradeViewModel ViewModel;
public ViewSubjectGradePage()
{
this.InitializeComponent();
this.navigationHelper = new NavigationHelper(this);
this.navigationHelper.LoadState += this.NavigationHelper_LoadState;
this.navigationHelper.SaveState += this.NavigationHelper_SaveState;
ViewModel = new ViewSubjectGradeViewModel();
this.DataContext = ViewModel;
}
/// <summary>
/// Gets the <see cref="NavigationHelper"/> associated with this <see cref="Page"/>.
/// </summary>
public NavigationHelper NavigationHelper
{
get { return this.navigationHelper; }
}
/// <summary>
/// Populates the page with content passed during navigation. Any saved state is also
/// provided when recreating a page from a prior session.
/// </summary>
/// <param name="sender">
/// The source of the event; typically <see cref="NavigationHelper"/>
/// </param>
/// <param name="e">Event data that provides both the navigation parameter passed to
/// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested and
/// a dictionary of state preserved by this page during an earlier
/// session. The state will be null the first time a page is visited.</param>
private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
}
/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache. Values must conform to the serialization
/// requirements of <see cref="SuspensionManager.SessionState"/>.
/// </summary>
/// <param name="sender">The source of the event; typically <see cref="NavigationHelper"/></param>
/// <param name="e">Event data that provides an empty dictionary to be populated with
/// serializable state.</param>
private void NavigationHelper_SaveState(object sender, SaveStateEventArgs e)
{
}
#region NavigationHelper registration
/// <summary>
/// The methods provided in this section are simply used to allow
/// NavigationHelper to respond to the page's navigation methods.
/// <para>
/// Page specific logic should be placed in event handlers for the
/// <see cref="NavigationHelper.LoadState"/>
/// and <see cref="NavigationHelper.SaveState"/>.
/// The navigation parameter is available in the LoadState method
/// in addition to page state preserved during an earlier session.
/// </para>
/// </summary>
/// <param name="e">Provides data for navigation methods and event
/// handlers that cannot cancel the navigation request.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.navigationHelper.OnNavigatedTo(e);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
this.navigationHelper.OnNavigatedFrom(e);
}
#endregion
}
}
And receiving the list in the ViewSubjectGradeVM via the constructor:
namespace LC_Points.ViewModel
{
public class ViewSubjectGradeViewModel
{
public ViewSubjectGradeViewModel()
{
}
/// <summary>
/// Initializes a new instance of the ViewSubjectGradeViewModel class.
/// </summary>
public ViewSubjectGradeViewModel(IEnumerable<ScoreModel> addedSubjectGradePairs)
{
this.AddedSubjectGradePairsCopy = addedSubjectGradePairs;
}
//Property for collection passed from MainViewModel
public IEnumerable<ScoreModel> AddedSubjectGradePairsCopy { get; set; }
}
}
And this is the backing Model for the List being passed from the MainVM to the ViewSubjectGradeVM:
namespace LC_Points.Model
{
public class ScoreModel : INotifyPropertyChanged
{
// The name of the subject.
public string Subject { get; set; }
// The points paired with each grade type.
public int Points { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Try something like this. Of course, your model should comes form some source which you should map to your viewmodel. But this illustrates you the way to set correct datacontext.
var scoremodels = new List<ScoreModel>
{
new ScoreModel {Subject = "Subj1", Points = 6},
new ScoreModel {Subject = "Subj2", Points = 3},
new ScoreModel {Subject = "Subj3", Points = 8},
}
ViewModel = new ViewSubjectGradeViewModel(scoreModels);
this.DataContext = ViewModel;
I'm playing with databinding on c# compact framework. I develop a simple form with a Textbox and a Label. I want to change the data binded to Textbox (bindModelTextBox) and show these changes by the Label (bindModelLabel), which is binded to the same data. Here is the code:
public partial class CreateShipment : Form {
//simple bean. Just one property: id, a string
private BasicShipmentBean toBindBasicShipment = null;
public CreateShipment() {
InitializeComponent();
BindingSource bsProva = new BindingSource();
toBindBasicShipment = new BasicShipmentBean();
toBindBasicShipment.id = "boo";
bsProva.Add(toBindBasicShipment);
bindModelLabel.DataBindings.Add("Text", bsProva, "id", true, DataSourceUpdateMode.OnPropertyChanged);
bindModelTextBox.DataBindings.Add("Text", bsProva, "id", true, DataSourceUpdateMode.OnPropertyChanged);
bindModelTextBox.LostFocus += textLoseFocus;
}
...
private void textLoseFocus(object sender, System.EventArgs e)
{
System.Diagnostics.Debug.WriteLine("focus lost. "+toBindBasicShipment.id);
}
When textbox loose focus I can see the data is updated in the bean, but, the label still shows bean's original id value. What am I missing?
You need to implement INotifyPropertyChanged on your BasicShipmentBean class. I forgot where exactly I found this originally, but here is an ObservableObject base class that implements INotifyPropertyChanged, that I use for all of my data sources.
public abstract class ObservableObject : INotifyPropertyChanged
{
#region Debugging Aides
/// <summary>
/// Warns the developer if this object does not have
/// a public property with the specified name. This
/// method does not exist in a Release build.
/// </summary>
[Conditional("DEBUG")]
[DebuggerStepThrough]
public virtual void VerifyPropertyName(string propertyName)
{
// Verify that the property name matches a real,
// public, instance property on this object.
if (TypeDescriptor.GetProperties(this)[propertyName] == null)
{
string msg = "Invalid property name: " + propertyName;
if (this.ThrowOnInvalidPropertyName)
throw new Exception(msg);
else
Debug.Fail(msg);
}
}
/// <summary>
/// Returns whether an exception is thrown, or if a Debug.Fail() is used
/// when an invalid property name is passed to the VerifyPropertyName method.
/// The default value is false, but subclasses used by unit tests might
/// override this property's getter to return true.
/// </summary>
protected virtual bool ThrowOnInvalidPropertyName { get; private set; }
#endregion // Debugging Aides
#region INotifyPropertyChanged Members
/// <summary>
/// Raises the PropertyChange event for the property specified
/// </summary>
/// <param name="propertyName">Property name to update. Is case-sensitive.</param>
public virtual void RaisePropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
OnPropertyChanged(propertyName);
}
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion // INotifyPropertyChanged Members
}
Then, you need to raise the OnPropertyChanged event in your setter for the id in BasicShipmentBean, e.g.:
private string _id;
public string id
{
get { return _id; }
set
{
if (value != _id)
{
_id = value;
OnPropertyChanged("id");
}
}
}
Data binding in the Compact Framework is a bit more tedious than in WPF, but much of the implementation is pretty similar.
I have several compass properties one for each side and a direction property. I have the direction bound to a combobox and I have a case statement in the setter of the direction to set the compass points.
The issue I am running into is that the UI isnt refreshing. If I close the form and reopen it, the data has changed to the correct values but the UI wont change dynamically.
What do i need to do?
For the Case you working with WPF, you are looking for the INotifyPropertyChanged interface that allows you to "send" a message with the Property to the UI.
Mostly its in a base that is Called ViewModelBase:
public class ViewModelBase : INotifyPropertyChanged
{
/// <summary>
/// Raised when a property on this object has a new value
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this ViewModels PropertyChanged event
/// </summary>
/// <param name="propertyName">Name of the property that has a new value</param>
protected void SendPropertyChanged(string propertyName)
{
SendPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Raises this ViewModels PropertyChanged event
/// </summary>
/// <param name="e">Arguments detailing the change</param>
protected virtual void SendPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
public void SendPropertyChanged<TProperty>(Expression<Func<TProperty>> property)
{
var lambda = (LambdaExpression)property;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = (UnaryExpression)lambda.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
}
else
{
memberExpression = (MemberExpression)lambda.Body;
}
SendPropertyChanged(memberExpression.Member.Name);
}
//.Net 4.5? or 4 and BCL included?
/// <summary>
/// Raises this ViewModels PropertyChanged event
/// </summary>
/// <param name="propertyName">Name of the property that has a new value</param>
protected void SendPropertyChanged([CallerMemberName]string propertyName = "")
{
SendPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
Write a SendPropertyChanged(() => Property) or a SendPropertyChanged("Property") into your Setter and the UI will be updated
I think the code is self-explanatory.
<Interactivity:Interaction.Triggers>
<Interactivity:EventTrigger EventName="Deleting">
<MVVMLight:EventToCommand Command="{Binding Deleting, Mode=OneWay}"
PassEventArgsToCommand="True" />
</Interactivity:EventTrigger>
</Interactivity:Interaction.Triggers>
I have my own custom control with a delete event and want to bind it to a command in the ViewModel.
But in the view model, I have now either
public void OnDeleting(EventArgs args)
{
var e = args as MapDeletingEventArgs;
if (e == null)
throw new ArgumentNullException("args");
Database.Delete(e.Maps);
Database.Commit();
}
or worse
public void OnDeleting(MapDeletingEventArgs args)
{
if (args == null)
throw new ArgumentNullException("args");
Database.Delete(args.Maps);
Database.Commit();
}
And I know how bad it is to have view logic in the ViewModel. I can think of no better way, does anyone advice? I use the framework MVVMLight as you can see maybe.
This can be achieved with an ICommand implementation that takes a Map instance as it's command parameter:
//WARNING: all code typed in SO window
public class DeleteMapsCommand : ICommand
{
private Database _db;
public DeleteMapsCommand(Database db)
{
_db = db;
}
public void CanExecute(object parameter)
{
//only allow delete if the parameter passed in is a valid Map
return (parameter is Map);
}
public void Execute(object parameter)
{
var map = parameter as Map;
if (map == null) return;
_db.Delete(map);
_db.Commit();
}
public event EventHandler CanExecuteChanged; //ignore this for now
}
You then create a public property in your view model to expose an instance of the command
public class ViewModel
{
public ViewModel() {
//get the Database reference from somewhere?
this.DeleteMapCommand = new DeleteMapsCommand(this.Database);
}
public ICommand DeleteMapCommand { get; private set; }
}
Finally you need to bind your action to the command property and bind the command property to the map to be deleted. You haven't really given me enough of your XAML to state how this should be done in your case, but you could do something like the below with a ListBox:
<ListBox x:Name="ListOfMaps" ItemsSource="{Binding AllTheMaps}" />
<Button Command="{Binding DeleteMapCommand}" CommandParameter="{Binding SelectedItem, ElementName=ListOfMaps}">Delete Selected Map</Button>
Update
To attach the command to the event you can use an attached property:
public static class Helper
{
public static IComparable GetDeleteMapCommand(DependencyObject obj)
{
return (IComparable)obj.GetValue(DeleteMapCommandProperty);
}
public static void SetDeleteMapCommand(DependencyObject obj, IComparable value)
{
obj.SetValue(DeleteMapCommandProperty, value);
}
public static readonly DependencyProperty DeleteMapCommandProperty =
DependencyProperty.RegisterAttached("DeleteMapCommand", typeof(IComparable), typeof(Helper), new UIPropertyMetadata(null, OnDeleteMapCommandChanged));
private static void OnDeleteMapCommandChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
//when we attach the command, grab a reference to the control
var mapControl = sender as MapControl;
if (mapControl == null) return;
//and the command
var command = GetDeleteMapCommand(sender);
if (command == null) return;
//then hook up the event handler
mapControl.Deleting += (o,e) =>
{
if (command.CanExecute(e.Maps))
command.Execute(e.Maps);
};
}
}
You then need to bind the command like this:
<MapControl local:Helper.DeleteMapCommand="{Binding DeleteMapCommand}" />
Now your view model has no reference to the view-specific types.
If you don't want to hand your EventArgs off to your viewmodel, you could try using a Behavior (this is similar to Steve Greatrex's solution, but uses the Blend SDK'a behavior instead):
Here is an example I use in one of my applications.
First, here's my custom behavior base class:
/// <summary>
/// "Better" Behavior base class which allows for safe unsubscribing. The default Behavior class does not always call <see cref="Behavior.OnDetaching"/>
/// </summary>
/// <typeparam name="T">The dependency object this behavior should be attached to</typeparam>
public abstract class ZBehaviorBase<T> : Behavior<T> where T : FrameworkElement
{
private bool _isClean = true;
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
protected sealed override void OnAttached()
{
base.OnAttached();
AssociatedObject.Unloaded += OnAssociatedObjectUnloaded;
_isClean = false;
ValidateRequiredProperties();
Initialize();
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
/// </summary>
/// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
protected sealed override void OnDetaching()
{
CleanUp();
base.OnDetaching();
}
/// <summary>
/// Validates the required properties. This method is called when the object is attached, but before
/// the <see cref="Initialize"/> is invoked.
/// </summary>
protected virtual void ValidateRequiredProperties()
{
}
/// <summary>
/// Initializes the behavior. This method is called instead of the <see cref="OnAttached"/> which is sealed
/// to protect the additional behavior.
/// </summary>
protected abstract void Initialize();
/// <summary>
/// Uninitializes the behavior. This method is called when <see cref="OnDetaching"/> is called, or when the
/// <see cref="AttachedControl"/> is unloaded.
/// <para />
/// If dependency properties are used, it is very important to use <see cref="ClearValue"/> to clear the value
/// of the dependency properties in this method.
/// </summary>
protected abstract void Uninitialize();
/// <summary>
/// Called when the <see cref="AssociatedObject"/> is unloaded.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void OnAssociatedObjectUnloaded(object sender, EventArgs e)
{
CleanUp();
}
/// <summary>
/// Actually cleans up the behavior because <see cref="OnDetaching"/> is not always called.
/// </summary>
/// <remarks>
/// This is based on the blog post: http://dotnetbyexample.blogspot.com/2011/04/safe-event-detachment-pattern-for.html.
/// </remarks>
private void CleanUp()
{
if (_isClean)
{
return;
}
_isClean = true;
if (AssociatedObject != null)
{
AssociatedObject.Unloaded -= OnAssociatedObjectUnloaded;
}
Uninitialize();
}
}
Now, my concrete implementation used to attach a command to the TextBlock's "click" event
public class TextBlockClickCommandBehavior : ZBehaviorBase<TextBlock>
{
public ICommand ClickCommand
{
get { return (ICommand)GetValue(ClickCommandProperty); }
set { SetValue(ClickCommandProperty, value); }
}
public static readonly DependencyProperty ClickCommandProperty =
DependencyProperty.Register("ClickCommand", typeof(ICommand), typeof(TextBlockClickCommandBehavior));
protected override void Initialize()
{
this.AssociatedObject.MouseLeftButtonUp += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
}
protected override void Uninitialize()
{
if (this.AssociatedObject != null)
{
this.AssociatedObject.MouseLeftButtonUp -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
}
}
void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// if you want to pass a command param to CanExecute, need to add another dependency property to bind to
if (ClickCommand != null && ClickCommand.CanExecute(null))
{
ClickCommand.Execute(null);
}
}
}
And I use it like this:
<!--Make the TextBlock for "Item" clickable to display the item search window-->
<TextBlock x:Name="itemTextBlock" Text="Item:" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Column="2" FontWeight="Bold">
<e:Interaction.Behaviors>
<ZViewModels:TextBlockClickCommandBehavior ClickCommand="{Binding Path=ItemSearchCommand}"/>
</e:Interaction.Behaviors>
</TextBlock>
Now, in your case, instead of passing NULL to the command's execute method, you'd want to pass your arguments' Maps collection
My combobox returns a set of values from s stored procedure as this
private void BindCombo()
{
DataCombo.FillCombo(ComboDS(2313001), cmbClass, 0);
DataCombo.FillCombo(DDCombo(5007), cmbGroup, 0);
}
I managed to give a rudimentary auto complete suggestion as IsTextSearchenabled but cannot get a auto suggestion box that i would like.
I have seen loads of examples of autocomplete/suggestive textboxes but none of them seem to suit me.
this code apparently suits me.
but how would i use the auto suggest here
using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DotNetZen.AutoFilteredComboBox
{
public class AutoFilteredComboBox : ComboBox
{
private int silenceEvents = 0;
/// <summary>
/// Creates a new instance of <see cref="AutoFilteredComboBox" />.
/// </summary>
public AutoFilteredComboBox()
{
DependencyPropertyDescriptor textProperty = DependencyPropertyDescriptor.FromProperty(
ComboBox.TextProperty, typeof(AutoFilteredComboBox));
textProperty.AddValueChanged(this, this.OnTextChanged);
this.RegisterIsCaseSensitiveChangeNotification();
}
#region IsCaseSensitive Dependency Property
/// <summary>
/// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
/// </summary>
public static readonly DependencyProperty IsCaseSensitiveProperty =
DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
/// <summary>
/// Gets or sets the way the combo box treats the case sensitivity of typed text.
/// </summary>
/// <value>The way the combo box treats the case sensitivity of typed text.</value>
[System.ComponentModel.Description("The way the combo box treats the case sensitivity of typed text.")]
[System.ComponentModel.Category("AutoFiltered ComboBox")]
[System.ComponentModel.DefaultValue(true)]
public bool IsCaseSensitive
{
[System.Diagnostics.DebuggerStepThrough]
get
{
return (bool)this.GetValue(IsCaseSensitiveProperty);
}
[System.Diagnostics.DebuggerStepThrough]
set
{
this.SetValue(IsCaseSensitiveProperty, value);
}
}
protected virtual void OnIsCaseSensitiveChanged(object sender, EventArgs e)
{
if (this.IsCaseSensitive)
this.IsTextSearchEnabled = false;
this.RefreshFilter();
}
private void RegisterIsCaseSensitiveChangeNotification()
{
System.ComponentModel.DependencyPropertyDescriptor.FromProperty(IsCaseSensitiveProperty, typeof(AutoFilteredComboBox)).AddValueChanged(
this, this.OnIsCaseSensitiveChanged);
}
#endregion
#region DropDownOnFocus Dependency Property
/// <summary>
/// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
/// </summary>
public static readonly DependencyProperty DropDownOnFocusProperty =
DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(true));
/// <summary>
/// Gets or sets the way the combo box behaves when it receives focus.
/// </summary>
/// <value>The way the combo box behaves when it receives focus.</value>
[System.ComponentModel.Description("The way the combo box behaves when it receives focus.")]
[System.ComponentModel.Category("AutoFiltered ComboBox")]
[System.ComponentModel.DefaultValue(true)]
public bool DropDownOnFocus
{
[System.Diagnostics.DebuggerStepThrough]
get
{
return (bool)this.GetValue(DropDownOnFocusProperty);
}
[System.Diagnostics.DebuggerStepThrough]
set
{
this.SetValue(DropDownOnFocusProperty, value);
}
}
#endregion
#region | Handle selection |
/// <summary>
/// Called when <see cref="ComboBox.ApplyTemplate()"/> is called.
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.EditableTextBox.SelectionChanged += this.EditableTextBox_SelectionChanged;
}
/// <summary>
/// Gets the text box in charge of the editable portion of the combo box.
/// </summary>
protected TextBox EditableTextBox
{
get
{
return ((TextBox)base.GetTemplateChild("PART_EditableTextBox"));
}
}
private int start = 0, length = 0;
private void EditableTextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
if (this.silenceEvents == 0)
{
this.start = ((TextBox)(e.OriginalSource)).SelectionStart;
this.length = ((TextBox)(e.OriginalSource)).SelectionLength;
this.RefreshFilter();
}
}
#endregion
#region | Handle focus |
/// <summary>
/// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
/// reaches this element in its route.
/// </summary>
/// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (this.ItemsSource != null && this.DropDownOnFocus)
{
this.IsDropDownOpen = true;
}
}
#endregion
#region | Handle filtering |
private void RefreshFilter()
{
if (this.ItemsSource != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
view.Refresh();
this.IsDropDownOpen = true;
}
}
private bool FilterPredicate(object value)
{
// We don't like nulls.
if (value == null)
return false;
// If there is no text, there's no reason to filter.
if (this.Text.Length == 0)
return true;
string prefix = this.Text;
// If the end of the text is selected, do not mind it.
if (this.length > 0 && this.start + this.length == this.Text.Length)
{
prefix = prefix.Substring(0, this.start);
}
return value.ToString()
.StartsWith(prefix, !this.IsCaseSensitive, CultureInfo.CurrentCulture);
}
#endregion
/// <summary>
/// Called when the source of an item in a selector changes.
/// </summary>
/// <param name="oldValue">Old value of the source.</param>
/// <param name="newValue">New value of the source.</param>
protected override void OnItemsSourceChanged(System.Collections.IEnumerable oldValue, System.Collections.IEnumerable newValue)
{
if (newValue != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += this.FilterPredicate;
}
if (oldValue != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
view.Filter -= this.FilterPredicate;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void OnTextChanged(object sender, EventArgs e)
{
if (!this.IsTextSearchEnabled && this.silenceEvents == 0)
{
this.RefreshFilter();
// Manually simulate the automatic selection that would have been
// available if the IsTextSearchEnabled dependency property was set.
if (this.Text.Length > 0)
{
foreach (object item in CollectionViewSource.GetDefaultView(this.ItemsSource))
{
int text = item.ToString().Length, prefix = this.Text.Length;
this.SelectedItem = item;
this.silenceEvents++;
this.EditableTextBox.Text = item.ToString();
this.EditableTextBox.Select(prefix, text - prefix);
this.silenceEvents--;
break;
}
}
}
}
}
}
Also found the AutoFilteredComboBox to be very simple to work with. Though I have made some changes:
Removed use of DependencyPropertyDescriptor to avoid memory leak of combobox-objects
Introduced FilterItem-event and FilterList-event to allow one to customize the filtering
Changed default filtering from starts-with-string to contains-string
Removed support of having IsTextSearchEnabled enabled
Shows dropdown as soon one changes the search string, so search result is displayed
Example of how it is used:
<Controls:AutoFilteredComboBox ItemsSource="{Binding ViewModel.AvailableItems}"
SelectedValue="{Binding ViewModel.SelectedItem, Mode=TwoWay}"
IsEditable="True" IsTextSearchEnabled="False"/>
Improved version of AutoFilteredComboBox:
public class AutoFilteredComboBox : ComboBox
{
bool _ignoreTextChanged;
string _currentText;
/// <summary>
/// Creates a new instance of <see cref="AutoFilteredComboBox" />.
/// </summary>
public AutoFilteredComboBox()
{
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) return;
}
public event Func<object, string, bool> FilterItem;
public event Action<string> FilterList;
#region IsCaseSensitive Dependency Property
/// <summary>
/// The <see cref="DependencyProperty"/> object of the <see cref="IsCaseSensitive" /> dependency property.
/// </summary>
public static readonly DependencyProperty IsCaseSensitiveProperty =
DependencyProperty.Register("IsCaseSensitive", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
/// <summary>
/// Gets or sets the way the combo box treats the case sensitivity of typed text.
/// </summary>
/// <value>The way the combo box treats the case sensitivity of typed text.</value>
[Description("The way the combo box treats the case sensitivity of typed text.")]
[Category("AutoFiltered ComboBox")]
[DefaultValue(true)]
public bool IsCaseSensitive
{
[System.Diagnostics.DebuggerStepThrough]
get
{
return (bool)this.GetValue(IsCaseSensitiveProperty);
}
[System.Diagnostics.DebuggerStepThrough]
set
{
this.SetValue(IsCaseSensitiveProperty, value);
}
}
#endregion
#region DropDownOnFocus Dependency Property
/// <summary>
/// The <see cref="DependencyProperty"/> object of the <see cref="DropDownOnFocus" /> dependency property.
/// </summary>
public static readonly DependencyProperty DropDownOnFocusProperty =
DependencyProperty.Register("DropDownOnFocus", typeof(bool), typeof(AutoFilteredComboBox), new UIPropertyMetadata(false));
/// <summary>
/// Gets or sets the way the combo box behaves when it receives focus.
/// </summary>
/// <value>The way the combo box behaves when it receives focus.</value>
[Description("The way the combo box behaves when it receives focus.")]
[Category("AutoFiltered ComboBox")]
[DefaultValue(false)]
public bool DropDownOnFocus
{
[System.Diagnostics.DebuggerStepThrough]
get
{
return (bool)this.GetValue(DropDownOnFocusProperty);
}
[System.Diagnostics.DebuggerStepThrough]
set
{
this.SetValue(DropDownOnFocusProperty, value);
}
}
#endregion
#region | Handle focus |
/// <summary>
/// Invoked whenever an unhandled <see cref="UIElement.GotFocus" /> event
/// reaches this element in its route.
/// </summary>
/// <param name="e">The <see cref="RoutedEventArgs" /> that contains the event data.</param>
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (this.ItemsSource != null && this.DropDownOnFocus)
{
this.IsDropDownOpen = true;
}
}
#endregion
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AddHandler(TextBox.TextChangedEvent, new TextChangedEventHandler(OnTextChanged));
KeyUp += AutoFilteredComboBox_KeyUp;
this.IsTextSearchEnabled = false;
}
void AutoFilteredComboBox_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Down)
{
if (this.IsDropDownOpen == true)
{
// Ensure that focus is given to the dropdown list
if (Keyboard.FocusedElement is TextBox)
{
Keyboard.Focus(this);
if (this.Items.Count > 0)
{
if (this.SelectedIndex == -1 || this.SelectedIndex==0)
this.SelectedIndex = 0;
}
}
}
}
if (Keyboard.FocusedElement is TextBox)
{
if (e.OriginalSource is TextBox)
{
// Avoid the automatic selection of the first letter (As next letter will cause overwrite)
TextBox textBox = e.OriginalSource as TextBox;
if (textBox.Text.Length == 1 && textBox.SelectionLength == 1)
{
textBox.SelectionLength = 0;
textBox.SelectionStart = 1;
}
}
}
}
#region | Handle filtering |
private void RefreshFilter()
{
if (this.ItemsSource != null)
{
Action<string> filterList = FilterList;
if (filterList != null)
{
filterList(_currentText);
}
else
{
ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
view.Refresh();
}
this.SelectedIndex = -1; // Prepare so arrow down selects first
this.IsDropDownOpen = true;
}
}
private bool FilterPredicate(object value)
{
// We don't like nulls.
if (value == null)
return false;
// If there is no text, there's no reason to filter.
if (string.IsNullOrEmpty(_currentText))
return true;
Func<object, string, bool> filterItem = FilterItem;
if (filterItem != null)
return filterItem(value, _currentText);
if (IsCaseSensitive)
return value.ToString().Contains(_currentText);
else
return value.ToString().ToUpper().Contains(_currentText.ToUpper());
}
#endregion
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
try
{
_ignoreTextChanged = true; // Ignore the following TextChanged
base.OnSelectionChanged(e);
}
finally
{
_ignoreTextChanged = false;
}
}
/// <summary>
/// Called when the source of an item in a selector changes.
/// </summary>
/// <param name="oldValue">Old value of the source.</param>
/// <param name="newValue">New value of the source.</param>
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
if (FilterList == null)
view.Filter += this.FilterPredicate;
}
if (oldValue != null)
{
ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
view.Filter -= this.FilterPredicate;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (_ignoreTextChanged)
return;
_currentText = Text;
if (!this.IsTextSearchEnabled)
{
this.RefreshFilter();
}
}
I have found a super simple workaround to my problem.
i created a preview text input event of the combobox.
then i just wrote
Combobox.IsDropDownOpen = true
may not the most elegant but works in my case