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;
Related
I have a form where users set parameters for a numerical process. Each parameter object has a default value.
public double DefaultValue
{
get => _defaultValue;
set
{
_defaultValue = value;
OnPropertyChanged("DefaultValue");
}
}
Although the property is a double, it might represent a Boolean, or an integer. For most parameters validation is not required, but I have two parameters, Min and Max, which are limited. I must not have Min > Max or Max < Min. I have implemented validation in XAML, which visually warns the user if the data is not valid. The data template for the Min parameter is as follows.
<DataTemplate x:Key="MinParameterDataTemplateThin">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding DisplayName, StringFormat='{}{0}:'}" Grid.Column="0" Margin="10,5,5,10" VerticalAlignment="Top" TextWrapping="Wrap"
Visibility="{Binding Visibility}" ToolTipService.ShowDuration="20000">
<TextBlock.ToolTip>
<ToolTip DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock Text="{Binding Description}"/>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBox Name ="MinTextBox" Margin="5" Width="50" VerticalAlignment="Top"
Visibility="{Binding Visibility}" IsEnabled="{Binding IsEnabled}">
<TextBox.Resources>
<validations:BindingProxy x:Key="proxy" Data="{Binding}"/>
</TextBox.Resources>
<TextBox.Text>
<Binding Path="DefaultValue" StringFormat="N2" Mode="TwoWay"
UpdateSourceTrigger="LostFocus"
ValidatesOnExceptions="True"
NotifyOnValidationError="True"
ValidatesOnNotifyDataErrors="True">
<Binding.ValidationRules>
<validations:MaximumValueValidation ValidatesOnTargetUpdated="True">
<validations:MaximumValueValidation.MaxValueWrapper>
<validations:MaxValueWrapper MaxValue="{Binding Data.MaxValue, Source={StaticResource proxy}}"/>
</validations:MaximumValueValidation.MaxValueWrapper>
</validations:MaximumValueValidation>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="{Binding UnitSymbol}" Margin="5" VerticalAlignment="Top" Visibility="{Binding Visibility}"/>
</StackPanel>
<Label Name="ValidationLabel" Content="{Binding ElementName=MinTextBox, Path=(Validation.Errors)[0].ErrorContent}" Foreground="Red" Grid.Row="1" VerticalAlignment="Top"/>
</Grid>
</Grid>
</DataTemplate>
There is a similar template for the Max parameter. In addition to the visual warning, I need to prevent the user from saving the data. I would like to have a Boolean IsValid property in the parameter object to test when the user tries to save. How would I bind from the XAML to this IsValid property?
When you see an error appear with a red border, there is a routed error event raised which will bubble up through the visual tree.
When you fix an error then you get the same event raised tells you that the error is fixed.
You can therefore add up errors occur and subtract errors fixed. If you have more than zero you have something needs fixing.
This is the Validation.ErrorEvent
https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.validation.error?view=windowsdesktop-7.0
You can then pass the result to the viewmodel or call a command to pass the event result and your viewmodel does that addition.
Here's some markup and code.
In a parent of all the controls which may error:
<i:Interaction.Triggers>
<UIlib:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
<e2c:EventToCommand
Command="{Binding ConversionErrorCommand, Mode=OneWay}"
EventArgsConverter="{StaticResource BindingErrorEventArgsConverter}"
PassEventArgsToCommand="True" />
</UIlib:RoutedEventTrigger>
Not sure this will still be copy paste friendly since it's a bit old now.
public RelayCommand<PropertyError> ConversionErrorCommand
{
get
{
return conversionErrorCommand
?? (conversionErrorCommand = new RelayCommand<PropertyError>
(PropertyError =>
{
if (PropertyError.Added)
{
AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
}
FlattenErrorList();
}));
}
}
Converter
public class BindingErrorEventArgsConverter : IEventArgsConverter
{
public object Convert(object value, object parameter)
{
ValidationErrorEventArgs e = (ValidationErrorEventArgs)value;
PropertyError err = new PropertyError();
err.PropertyName = ((System.Windows.Data.BindingExpression)(e.Error.BindingInError)).ResolvedSourcePropertyName;
err.Error = e.Error.ErrorContent.ToString();
// Validation.ErrorEvent fires both when an error is added AND removed
if (e.Action == ValidationErrorEventAction.Added)
{
err.Added = true;
}
else
{
err.Added = false;
}
return err;
}
}
Routedeventtrigger
// This is necessary in order to grab the bubbling routed source changed and conversion errors
public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
{
RoutedEvent routedEvent;
public RoutedEvent RoutedEvent
{
get
{
return routedEvent;
}
set
{
routedEvent = value;
}
}
public RoutedEventTrigger()
{
}
protected override void OnAttached()
{
Behavior behavior = base.AssociatedObject as Behavior;
FrameworkElement associatedElement = base.AssociatedObject as FrameworkElement;
if (behavior != null)
{
associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
}
if (associatedElement == null)
{
throw new ArgumentException("This only works with framework elements");
}
if (RoutedEvent != null)
{
associatedElement.AddHandler(RoutedEvent, new RoutedEventHandler(this.OnRoutedEvent));
}
}
void OnRoutedEvent(object sender, RoutedEventArgs args)
{
base.OnEvent(args);
args.Handled = true;
}
protected override string GetEventName()
{
return RoutedEvent.Name;
}
}
Event to command is from mvvm light. Which is now deprecated but the code still works.
You might like
MVVM Passing EventArgs As Command Parameter
I think this explains a prism approach:
https://weblogs.asp.net/alexeyzakharov/silverlight-commands-hacks-passing-eventargs-as-commandparameter-to-delegatecommand-triggered-by-eventtrigger
But I think mvvmlight is open source, here's the code for eventtocommand:
// ****************************************************************************
// <copyright file="EventToCommand.cs" company="GalaSoft Laurent Bugnion">
// Copyright © GalaSoft Laurent Bugnion 2009-2016
// </copyright>
// ****************************************************************************
// <author>Laurent Bugnion</author>
// <email>laurent#galasoft.ch</email>
// <date>3.11.2009</date>
// <project>GalaSoft.MvvmLight.Extras</project>
// <web>http://www.mvvmlight.net</web>
// <license>
// See license.txt in this solution or http://www.galasoft.ch/license_MIT.txt
// </license>
// ****************************************************************************
using Microsoft.Xaml.Behaviors;
using System;
using System.Windows;
using System.Windows.Input;
////using GalaSoft.Utilities.Attributes;
namespace GalaSoft.MvvmLight.CommandWpf
{
/// <summary>
/// This <see cref="T:System.Windows.Interactivity.TriggerAction`1" /> can be
/// used to bind any event on any FrameworkElement to an <see cref="ICommand" />.
/// Typically, this element is used in XAML to connect the attached element
/// to a command located in a ViewModel. This trigger can only be attached
/// to a FrameworkElement or a class deriving from FrameworkElement.
/// <para>To access the EventArgs of the fired event, use a RelayCommand<EventArgs>
/// and leave the CommandParameter and CommandParameterValue empty!</para>
/// </summary>
////[ClassInfo(typeof(EventToCommand),
//// VersionString = "5.2.8",
//// DateString = "201504252130",
//// Description = "A Trigger used to bind any event to an ICommand.",
//// UrlContacts = "http://www.galasoft.ch/contact_en.html",
//// Email = "laurent#galasoft.ch")]
public class EventToCommand : TriggerAction<DependencyObject>
{
/// <summary>
/// Identifies the <see cref="CommandParameter" /> dependency property
/// </summary>
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
"CommandParameter",
typeof(object),
typeof(EventToCommand),
new PropertyMetadata(
null,
(s, e) =>
{
var sender = s as EventToCommand;
if (sender == null)
{
return;
}
if (sender.AssociatedObject == null)
{
return;
}
sender.EnableDisableElement();
}));
/// <summary>
/// Identifies the <see cref="Command" /> dependency property
/// </summary>
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(EventToCommand),
new PropertyMetadata(
null,
(s, e) => OnCommandChanged(s as EventToCommand, e)));
/// <summary>
/// Identifies the <see cref="MustToggleIsEnabled" /> dependency property
/// </summary>
public static readonly DependencyProperty MustToggleIsEnabledProperty = DependencyProperty.Register(
"MustToggleIsEnabled",
typeof(bool),
typeof(EventToCommand),
new PropertyMetadata(
false,
(s, e) =>
{
var sender = s as EventToCommand;
if (sender == null)
{
return;
}
if (sender.AssociatedObject == null)
{
return;
}
sender.EnableDisableElement();
}));
private object _commandParameterValue;
private bool? _mustToggleValue;
/// <summary>
/// Gets or sets the ICommand that this trigger is bound to. This
/// is a DependencyProperty.
/// </summary>
public ICommand Command
{
get
{
return (ICommand) GetValue(CommandProperty);
}
set
{
SetValue(CommandProperty, value);
}
}
/// <summary>
/// Gets or sets an object that will be passed to the <see cref="Command" />
/// attached to this trigger. This is a DependencyProperty.
/// </summary>
public object CommandParameter
{
get
{
return GetValue(CommandParameterProperty);
}
set
{
SetValue(CommandParameterProperty, value);
}
}
/// <summary>
/// Gets or sets an object that will be passed to the <see cref="Command" />
/// attached to this trigger. This property is here for compatibility
/// with the Silverlight version. This is NOT a DependencyProperty.
/// For databinding, use the <see cref="CommandParameter" /> property.
/// </summary>
public object CommandParameterValue
{
get
{
return _commandParameterValue ?? CommandParameter;
}
set
{
_commandParameterValue = value;
EnableDisableElement();
}
}
/// <summary>
/// Gets or sets a value indicating whether the attached element must be
/// disabled when the <see cref="Command" /> property's CanExecuteChanged
/// event fires. If this property is true, and the command's CanExecute
/// method returns false, the element will be disabled. If this property
/// is false, the element will not be disabled when the command's
/// CanExecute method changes. This is a DependencyProperty.
/// </summary>
public bool MustToggleIsEnabled
{
get
{
return (bool) GetValue(MustToggleIsEnabledProperty);
}
set
{
SetValue(MustToggleIsEnabledProperty, value);
}
}
/// <summary>
/// Gets or sets a value indicating whether the attached element must be
/// disabled when the <see cref="Command" /> property's CanExecuteChanged
/// event fires. If this property is true, and the command's CanExecute
/// method returns false, the element will be disabled. This property is here for
/// compatibility with the Silverlight version. This is NOT a DependencyProperty.
/// For databinding, use the <see cref="MustToggleIsEnabled" /> property.
/// </summary>
public bool MustToggleIsEnabledValue
{
get
{
return _mustToggleValue == null
? MustToggleIsEnabled
: _mustToggleValue.Value;
}
set
{
_mustToggleValue = value;
EnableDisableElement();
}
}
/// <summary>
/// Called when this trigger is attached to a FrameworkElement.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
EnableDisableElement();
}
#if SILVERLIGHT
private Control GetAssociatedObject()
{
return AssociatedObject as Control;
}
#else
/// <summary>
/// This method is here for compatibility
/// with the Silverlight version.
/// </summary>
/// <returns>The FrameworkElement to which this trigger
/// is attached.</returns>
private FrameworkElement GetAssociatedObject()
{
return AssociatedObject as FrameworkElement;
}
#endif
/// <summary>
/// This method is here for compatibility
/// with the Silverlight 3 version.
/// </summary>
/// <returns>The command that must be executed when
/// this trigger is invoked.</returns>
private ICommand GetCommand()
{
return Command;
}
/// <summary>
/// Specifies whether the EventArgs of the event that triggered this
/// action should be passed to the bound RelayCommand. If this is true,
/// the command should accept arguments of the corresponding
/// type (for example RelayCommand<MouseButtonEventArgs>).
/// </summary>
public bool PassEventArgsToCommand
{
get;
set;
}
/// <summary>
/// Gets or sets a converter used to convert the EventArgs when using
/// <see cref="PassEventArgsToCommand"/>. If PassEventArgsToCommand is false,
/// this property is never used.
/// </summary>
public IEventArgsConverter EventArgsConverter
{
get;
set;
}
/// <summary>
/// The <see cref="EventArgsConverterParameter" /> dependency property's name.
/// </summary>
public const string EventArgsConverterParameterPropertyName = "EventArgsConverterParameter";
/// <summary>
/// Gets or sets a parameters for the converter used to convert the EventArgs when using
/// <see cref="PassEventArgsToCommand"/>. If PassEventArgsToCommand is false,
/// this property is never used. This is a dependency property.
/// </summary>
public object EventArgsConverterParameter
{
get
{
return GetValue(EventArgsConverterParameterProperty);
}
set
{
SetValue(EventArgsConverterParameterProperty, value);
}
}
/// <summary>
/// Identifies the <see cref="EventArgsConverterParameter" /> dependency property.
/// </summary>
public static readonly DependencyProperty EventArgsConverterParameterProperty = DependencyProperty.Register(
EventArgsConverterParameterPropertyName,
typeof(object),
typeof(EventToCommand),
new PropertyMetadata(null));
/// <summary>
/// The <see cref="AlwaysInvokeCommand" /> dependency property's name.
/// </summary>
public const string AlwaysInvokeCommandPropertyName = "AlwaysInvokeCommand";
/// <summary>
/// Gets or sets a value indicating if the command should be invoked even
/// if the attached control is disabled. This is a dependency property.
/// </summary>
public bool AlwaysInvokeCommand
{
get
{
return (bool)GetValue(AlwaysInvokeCommandProperty);
}
set
{
SetValue(AlwaysInvokeCommandProperty, value);
}
}
/// <summary>
/// Identifies the <see cref="AlwaysInvokeCommand" /> dependency property.
/// </summary>
public static readonly DependencyProperty AlwaysInvokeCommandProperty = DependencyProperty.Register(
AlwaysInvokeCommandPropertyName,
typeof(bool),
typeof(EventToCommand),
new PropertyMetadata(false));
/// <summary>
/// Provides a simple way to invoke this trigger programatically
/// without any EventArgs.
/// </summary>
public void Invoke()
{
Invoke(null);
}
/// <summary>
/// Executes the trigger.
/// <para>To access the EventArgs of the fired event, use a RelayCommand<EventArgs>
/// and leave the CommandParameter and CommandParameterValue empty!</para>
/// </summary>
/// <param name="parameter">The EventArgs of the fired event.</param>
protected override void Invoke(object parameter)
{
if (AssociatedElementIsDisabled()
&& !AlwaysInvokeCommand)
{
return;
}
var command = GetCommand();
var commandParameter = CommandParameterValue;
if (commandParameter == null
&& PassEventArgsToCommand)
{
commandParameter = EventArgsConverter == null
? parameter
: EventArgsConverter.Convert(parameter, EventArgsConverterParameter);
}
if (command != null
&& command.CanExecute(commandParameter))
{
command.Execute(commandParameter);
}
}
private static void OnCommandChanged(
EventToCommand element,
DependencyPropertyChangedEventArgs e)
{
if (element == null)
{
return;
}
if (e.OldValue != null)
{
((ICommand) e.OldValue).CanExecuteChanged -= element.OnCommandCanExecuteChanged;
}
var command = (ICommand) e.NewValue;
if (command != null)
{
command.CanExecuteChanged += element.OnCommandCanExecuteChanged;
}
element.EnableDisableElement();
}
private bool AssociatedElementIsDisabled()
{
var element = GetAssociatedObject();
return AssociatedObject == null
|| (element != null
&& !element.IsEnabled);
}
private void EnableDisableElement()
{
var element = GetAssociatedObject();
if (element == null)
{
return;
}
var command = GetCommand();
if (MustToggleIsEnabledValue
&& command != null)
{
element.IsEnabled = command.CanExecute(CommandParameterValue);
}
}
private void OnCommandCanExecuteChanged(object sender, EventArgs e)
{
EnableDisableElement();
}
}
}
I think you may need ieventargsconverter
namespace GalaSoft.MvvmLight.CommandWpf
{
/// <summary>
/// The definition of the converter used to convert an EventArgs
/// in the <see cref="EventToCommand"/> class, if the
/// <see cref="EventToCommand.PassEventArgsToCommand"/> property is true.
/// Set an instance of this class to the <see cref="EventToCommand.EventArgsConverter"/>
/// property of the EventToCommand instance.
/// </summary>
////[ClassInfo(typeof(EventToCommand))]
public interface IEventArgsConverter
{
/// <summary>
/// The method used to convert the EventArgs instance.
/// </summary>
/// <param name="value">An instance of EventArgs passed by the
/// event that the EventToCommand instance is handling.</param>
/// <param name="parameter">An optional parameter used for the conversion. Use
/// the <see cref="EventToCommand.EventArgsConverterParameter"/> property
/// to set this value. This may be null.</param>
/// <returns>The converted value.</returns>
object Convert(object value, object parameter);
}
}
I am attempting to write a simple helper class that will make managing Telerik RadGrids in our application consistent.
The relevant portions of the class are shown below:
public delegate object NeedDataSource(object aSender);
public class TelerikGridViewHelper
{
private readonly string _CommandNameDelete; //String checked for in ItemCommand to determine if we have a delete button click
private readonly string _CommandNameEdit; //String checked for in ItemCommand to determine if we have a edit button click
private readonly RadGrid _GridView;
private NeedDataSource _NeedDataSourceCallBack;
private RecordDelete _RecordDeleteCallback;
private RecordEdit _RecordEditCallback;
/// <summary>
/// Class constructor
/// </summary>
/// <param name="aGridView"></param>
public TelerikGridViewHelper(RadGrid aGridView)
{
//Save params supplied
_GridView = aGridView;
//Create resources required
_CommandNameDelete = Guid.NewGuid() + "_Delete";
_CommandNameEdit = Guid.NewGuid() + "_Edit";
Options = new TelerikGridViewHelperOptions();
}
/// <summary>
/// Defines how the grid view will be configured when the Configure() method is called.
/// </summary>
public TelerikGridViewHelperOptions Options { get; set; }
/// <summary>
/// Call this method after setting the appropriate Options to configure the grid view behaviours.
/// </summary>
/// <param name="aKeyFieldNames">List of fields that uniquely identify each record.</param>
/// <param name="aNeedDataSourceCallback">Method returning the contents of the datasource to be displayed by the grid.</param>
/// <param name="aRecordDeleteCallback">Called when a record is to be deleted as a consequence of the user pressing the Delete button automatically added by this class when Options.CommandDelete is true.</param>
/// <param name="aRecordEditCallback">Called when a record is to be edit as a consequence of the user pressing the Edit button automatically added by this class when Options.CommandEdit is true.</param>
public void Configure(string[] aKeyFieldNames, NeedDataSource aNeedDataSourceCallback, RecordDelete aRecordDeleteCallback, RecordEdit aRecordEditCallback)
{
//Save / Apply the parameters supplied
//Datasource
_NeedDataSourceCallBack = aNeedDataSourceCallback;
_RecordDeleteCallback = aRecordDeleteCallback;
_RecordEditCallback = aRecordEditCallback;
//Key fields
_GridView.MasterTableView.DataKeyNames = aKeyFieldNames;
//Now configure the grid based on the configured behaviours
ConfigureGrid(_GridView, Options);
}
/// <summary>
/// Internal helper method to configure the various aspects of the grid based on the options specified.
/// </summary>
/// <param name="aGridView">Gridview to configure.</param>
/// <param name="aOptions">Options to use when configuring the grid.</param>
private void ConfigureGrid(RadGrid aGridView, TelerikGridViewHelperOptions aOptions)
{
//Grid Options
aGridView.AllowFilteringByColumn = aOptions.Filtering;
aGridView.AllowPaging = aOptions.Paging;
aGridView.AutoGenerateColumns = aOptions.AutoGenerateColumns;
aGridView.AutoGenerateHierarchy = true;
aGridView.EnableHeaderContextMenu = true;
aGridView.GroupPanelPosition = GridGroupPanelPosition.Top;
aGridView.Skin = aOptions.ThemeName;
//Table view options
aGridView.MasterTableView.PagerStyle.AlwaysVisible = true;
aGridView.MasterTableView.PagerStyle.Position = GridPagerPosition.TopAndBottom;
aGridView.MasterTableView.PageSize = aOptions.PageSize;
//Events hookup
_GridView.NeedDataSource += CustomGridViewOnNeedDataSource;
}
/// <summary>
/// Hooked to the managed grid view's OnNeedDataSource event. We use this to call what the developer has supplied for
/// the datasource callback.
/// </summary>
/// <param name="aSender">Gridview instance that has fired this event</param>
/// <param name="aEventArgs">Contains the information about the need for the rebind of the datasource.</param>
private void CustomGridViewOnNeedDataSource(object aSender, GridNeedDataSourceEventArgs aEventArgs)
{
(aSender as RadGrid).DataSource = _NeedDataSourceCallBack;
}
/// <summary>
/// Internal helper method to raise the exception class associated with this class
/// </summary>
/// <param name="aExceptionMessage">Message to use when raising the exception.</param>
private void RaiseException(string aExceptionMessage)
{
throw new TelerikGridViewHelperException(aExceptionMessage);
}
}
The class is then invoked from code behind as follows:
public partial class TimeSheetList : BasePage
{
private readonly TimeSheetProcess _TimeSheetProcess;
/// <summary>
/// Class constructor
/// </summary>
public TimeSheetList()
{
_TimeSheetProcess = new TimeSheetProcess();
}
/// <summary>
/// Called when the page is loaded
/// </summary>
/// <param name="aSender"></param>
/// <param name="aEventArgs"></param>
protected void Page_Load(object aSender, EventArgs aEventArgs)
{
TelerikGridViewHelper gridViewHelper = new TelerikGridViewHelper(RadGrid2);
gridViewHelper.Configure(new []{"TimeSheetId"}, DataRetrieve, null, null);
}
private object DataRetrieve(object aSender)
{
//List of timesheets ordered by date and then user within that
DateTime todaysDate = DateTime.Now;
//List of page contents ordered by url
List<Timesheet> timesheetsFromDb = _TimeSheetProcess.GetTimeSheets()
.ToList();
MembershipCache membershipCache = new MembershipCache();
ClockedInBrowseViewModel timesheetsViewModel = new ClockedInBrowseViewModel();
foreach (Timesheet timeSheet in timesheetsFromDb)
{
MembershipUser userInfo = membershipCache.GetUserById(timeSheet.UserId.ToString());
timesheetsViewModel.Items.Add(new ClockedInItemViewModel
{
ClockedInAt = timeSheet.Start.HasValue ? string.Format("{0:dd MMM HH:mm}", timeSheet.Start) : "-",
ClockedOutAt = timeSheet.Finish.HasValue ? string.Format("{0:dd MMM HH:mm}", timeSheet.Finish) : "-",
TimesheetId = timeSheet.TimesheetID,
Username = userInfo.UserName
});
}
return timesheetsViewModel.Items;
}
}
Now the issue that I am having is that the passed in TimeSheetList.DataRetrieve() is not being called and I get the exception "Data source is an invalid type. It must be either an IListSource, IEnumerable, or IDataSource" as soon as the line "(aSender as RadGrid).DataSource = _NeedDataSourceCallBack;" is hit in the helper class. So I am guessing that I have not setup either the hooking on the NeedDataSource event or how the callback is invoked as I know that DataRetrieve() is returning a suitable list of objects.
Can someone point me in the right direction please?
Needed to call Invoke() as follows:
(aSender as RadGrid).DataSource = _NeedDataSourceCallBack.Invoke(aSender);
I am attempting to build an MVVM Windows Application Using PRISM 5 and I have wrapped my main content window with an AvalonDock (Wrapping Code Below).
using Microsoft.Practices.Prism.Regions;
using Xceed.Wpf.AvalonDock;
using System.Collections.Specialized;
using System.Windows;
using Xceed.Wpf.AvalonDock.Layout;
namespace Central.Adapters
{
using System.Linq;
public class AvalonDockRegionAdapter : RegionAdapterBase<DockingManager>
{
/// <summary>
/// This ties the adapter into the base region factory.
/// </summary>
/// <param name="factory">The factory that determines where the modules will go.</param>
public AvalonDockRegionAdapter(IRegionBehaviorFactory factory)
: base(factory)
{
}
/// <summary>
/// Since PRISM does not support the Avalon DockingManager natively this adapter provides the needed support.
/// </summary>
/// <param name="region">This is the region that resides in the DockingManager.</param>
/// <param name="regionTarget">The DockingManager that needs the window added.</param>
protected override void Adapt(IRegion region, DockingManager regionTarget)
{
region.Views.CollectionChanged += (sender, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddAnchorableDocument(regionTarget, e);
break;
case NotifyCollectionChangedAction.Remove:
break;
}
};
}
/// <summary>
/// This adds the window as an anchorable document to the Avalon DockingManager.
/// </summary>
/// <param name="regionTarget">The DockingManager instance.</param>
/// <param name="e">The new window to be added.</param>
private static void AddAnchorableDocument(DockingManager regionTarget, NotifyCollectionChangedEventArgs e)
{
foreach (FrameworkElement element in e.NewItems)
{
var view = element as UIElement;
var documentPane = regionTarget.Layout.Descendents().OfType<LayoutDocumentPane>().FirstOrDefault();
if ((view == null) || (documentPane == null))
{
continue;
}
var newContentPane = new LayoutAnchorable
{
Content = view,
Title = element.ToolTip.ToString(),
CanHide = true,
CanClose = false
};
documentPane.Children.Add(newContentPane);
}
}
/// <summary>
/// This returns the region instance populated with all of its contents.
/// </summary>
/// <returns>DockingManager formatted region.</returns>
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
}
I then register this adapter in the bootstrapper this way:
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
var mappings = base.ConfigureRegionAdapterMappings();
if (mappings == null)
{
return null;
}
mappings.RegisterMapping(typeof(DockingManager), new AvalonDockRegionAdapter(ConfigureDefaultRegionBehaviors()));
return mappings;
}
The problem that I am facing is that other region UI elements will require certain LayoutAnchorable windows to become the active and selected window. The content I am feeding into the LayoutAnchorable object is a ContentControl.
In my View's ViewModel I have a property that I am successfully setting using another UI element's interaction. However I am unable to make the connection from ViewModel(Property) -> ContentContro(View) -> LayoutAnchorable(View's Parent).IsSelected or ,IsActive.
I know how to bind to a parent object but that eats up the property and does not allow me to bind it to the ViewModel property as well. I also have no problem binding to a ViewModel property, but that is useless unless I can get it to set the parent property. I have also attempted View based events. Problem with this is that once the view loads it doe not like calling its own events anymore unless it is caused by user interaction directly with that view.
In short I just want to display the appropriate window when needed based on an interaction in another part of my program. Maybe I am making this more complicated than it needs to be. Any assistance on this would be greatly appreciated.
Thanks
James
As I took a break from the problem at had I looked at it from another perspective. To solve the issue I decided to store the instance of the content panes containing the views into a singleton dictionary class:
using System;
using System.Collections.Generic;
using Xceed.Wpf.AvalonDock.Layout;
namespace Central.Services
{
public class DockinWindowChildObjectDictionary
{
private static Dictionary<string, LayoutAnchorable> _contentPane = new Dictionary<string, LayoutAnchorable>();
private static readonly Lazy<DockinWindowChildObjectDictionary> _instance =
new Lazy<DockinWindowChildObjectDictionary>(()=> new DockinWindowChildObjectDictionary(), true);
public static DockinWindowChildObjectDictionary Instance
{
get
{
return _instance.Value;
}
}
/// <summary>
/// Causes the constructor to be private allowing for proper use of the Singleton pattern.
/// </summary>
private DockinWindowChildObjectDictionary()
{
}
/// <summary>
/// Adds a Content Pane instance to the dictionary.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
/// <param name="contentPane">The object instance.</param>
public static void Add(string title, LayoutAnchorable contentPane)
{
_contentPane.Add(title, contentPane);
}
/// <summary>
/// If a window needs to be removed from the dock this should be used
/// to also remove it from the dictionary.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
public static void Remove(string title)
{
_contentPane.Remove(title);
}
/// <summary>
/// This will return the instance of the content pane that holds the view.
/// </summary>
/// <param name="title">The title given to the Pane during instantiation.</param>
/// <returns>The views Parent Instance.</returns>
public static LayoutAnchorable GetInstance(string title)
{
return _contentPane[title];
}
}
}
In the adapter I modified this code as follows:
private static void AddAnchorableDocument(DockingManager regionTarget, NotifyCollectionChangedEventArgs e)
{
foreach (FrameworkElement element in e.NewItems)
{
var view = element as UIElement;
var documentPane = regionTarget.Layout.Descendents().OfType<LayoutDocumentPane>().FirstOrDefault();
if ((view == null) || (documentPane == null))
{
continue;
}
var newContentPane = new LayoutAnchorable
{
Content = view,
Title = element.ToolTip.ToString(),
CanHide = true,
CanClose = false
};
DockinWindowChildObjectDictionary.Add(element.ToolTip.ToString(),** newContentPane);
documentPane.Children.Add(newContentPane);
}
}
Then I added the following to the ViewModel to gain the effect I was going after:
public void OnNavigatedTo(NavigationContext navigationContext)
{
var viewParentInstance = DockinWindowChildObjectDictionary.GetInstance("Belt Plan");
viewParentInstance.IsSelected = true;
}
One hurdle done and on to the next. For a base to all the information in this post the ViewSwitchingNavigation.sln included with the PRISM 5.0 download will get you started. If you are wondering about the ConfigureDefaultRegionBehaviors() referenced in the adapter registration I got that from the StockTraderRI_Desktop.sln in the sample downloads.
I hope this post helps someone else that finds themselves in the same pickle this technology sandwich provides.
Sincerely
James
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
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"