I'm attempting to track the dirty (modified) status of a form in UWP that uses two way bindings between the view and the model.
My current method of tracking the respective changed events (TextChange for text boxes, SelectionChanged for combo boxes, etc.) works except for when the page is loaded with data.
What appears to be happening is that the bindings are still being evaluated after the page is fully loaded, thus making the form think it has been modified. From stepping through my code I'm building the following typical timeline when a page is loaded:
Model is initialized (dirty = false)
Data is retrieved and placed in the model
Bindings trigger, filling the form (dirty = true)
The Loaded event of the page is hit, at which point I set dirty = false
Bindings continue to be set, thus resetting dirty to true
(I need something here to set dirty back to false)
Is there an event that triggers after all bindings are evaluated for the first time, or some way of tracking whether the bindings have all been evaluated?
Edit: Some rough sample code to give a better idea of how I've got things set up
Model:
public class OrderModel : INotifyPropertyChanged
{
private OrderCase order;
public OrderCase Order
{
get
{
return order;
}
set
{
order = value;
this.RaisePropertyChanged("Order");
}
}
private bool dirty;
public bool Dirty
{
get
{
return dirty;
}
set
{
dirty = value;
}
}
}
View - Code Behind
OrderModel Model;
int OrderId;
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
//Call a method in the View Model which calls a WCF service
//to get the OrderCase object out of our database and into the
//Order object in the model
Model = new OrderModel();
Model.Order = await WcfService.GetOrder(OrderId);
//Once this returns all the bindings start evaluating
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
SetUndirty();
}
public override void TextBoxSetDirty(object sender, TextChangedEventArgs e)
{
//one of these exists for each type of control I use
//this gets triggered when the text changes due to the model changing
SetDirty();
}
public void SetDirty()
{
Model.Dirty = true;
}
View - XAML
<TextBox
Text = "{x:Bind Path=Order.CustomerName, mode=TwoWay}"
TextChanged="TextBoxSetDirty"/>
The view is a lot more of the same, including ComboBoxes, RadioButtons, ToggleSwitches, and a SyncFusion Datagrid
Edit 2:
I currently have a work around in place, another property called "UserHasInteracted" which is initially False, but upon either the page's KeyDown or Tapped events firing, it is set to true, and the SetDirty method is modified as such:
public void SetDirty()
{
if (UserHasInteracted)
Model.Dirty = true;
}
The end result is that the form can't be set to Dirty until the user has performed some sort of interaction upon it.
This works about 75% of the time, and produces no false positives (saying the form is dirty when it isn't), but does produce some false negatives, because the Tapped event doesn't seem to fire when a RadioButton, CheckBox, or ToggleSwitch is clicked. It also fails if the only interaction the user performs is a single key press within a text box, as the TextChanged event fires before the KeyDown, however the chances that our users will only enter a single character into a text box as their modification is slim to none, so this one isn't a major concern.
How is your ViewModel instatiated?
For your class:
public class OrderModel : INotifyPropertyChanged
{
private OrderCase order;
public OrderCase Order
{
get
{
return order;
}
set
{
if (value != order) { IsDirty = true; }
order = value;
this.RaisePropertyChanged("Order");
}
}
private bool dirty;
public bool Dirty
{
get
{
return dirty;
}
set
{
dirty = value;
}
}
}
This should work, or not?
OrderModel Model;
int OrderId;
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
//Call a method in the View Model which calls a WCF service
//to get the OrderCase object out of our database and into the
//Order object in the model
var tempModel = new OrderModel();
tempModel = await WcfService.GetOrder(OrderId);
Model.Order = tempModel;
//Setting the ViewModel's property only when getting order is completed
//Once this returns all the bindings start evaluating
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
SetUndirty();
}
public override void TextBoxSetDirty(object sender, TextChangedEventArgs e)
{
//one of these exists for each type of control I use
//this gets triggered when the text changes due to the model changing
SetDirty();
}
public void SetDirty()
{
Model.Dirty = true;
}
Otherwise you could try to add each single property in your ViewModel instead only one copy of OrderCase... just a thought
Related
I have the following code in the ViewModel class, in the constructor where I define that the buttons are always Enabled = false when starting the form ...
public partial class EditarConceptoWindow : ChildWindow
{
public EditarConceptoWindow(string documentoId)
{
InitializeComponent();
viewModel.Saved += new EventHandler<Microsoft.Practices.Prism.Events.DataEventArgs<bool>>(ViewModel_Saved);
viewModel.Calculation += new EventHandler<Microsoft.Practices.Prism.Events.DataEventArgs<bool>>(ViewModel_Calculation);
this.DataContext = viewModel;
BtnCalcular.IsEnabled = false;
BtnObtenerTCRM.IsEnabled = false;
....... rest of code
In a checked event of a check box when placing the Selected check box, it must be enabled to be set to true, depending on whether a particular element of a combobox has been selected as well;
private void cbAgregarManual_Checked(object sender, RoutedEventArgs e)
{
if (this.ComboConcepto.SelectedValue.ToString() == "DPI")
{
BtnCalcular.IsEnabled = true;
BtnObtenerTCRM.IsEnabled= true;
}
}
This must be done if and only if the checkbox is clicked and the DPI value is selected in the combobox.
But the behavior of the buttons is that when starting the form they are always IsEnabled = true and if the checkbox control is clicked if it works but I can't find a reason because only until I click the checkbox it works, there are some controls (such as TextBoxes, and also the buttons) with this directive in the XAML.
IsEnabled="{Binding ElementName=cbAgregarManual, Path=IsChecked }"
I suggest that you centralize the logic of the enabling into one property to avoid this mismatch of logic setting confusion.
In this new property it will use INotifyPropertyChanged for the notification of that change, but called in from other properties. So to sum up, when any of the associated values change, they do the notify call on the logic property; that process ensures that the control is properly en/dis-abled.
Example
Such as this pseudo code where I check three different other properties:
public bool IsEnabledCombo { get { return ClickStatus
&& OtherStatus
&& !string.IsNullOrEmpty( UserText); }
public bool ClickStatus { get { return _clickStatus; }
set { _clickStatus = value;
NotifyPropertyChanged("ClickStatus");
NotifyPropertyChanged("IsEnabledCombo");
}}
public bool OtherStatus { get { return _otherStatus; }
set { _clickStatus = value;
NotifyPropertyChanged("OtherStatus");
NotifyPropertyChanged("IsEnabledCombo");
}}
public string UserText { ...
set { _userText = value;
NotifyPropertyChanged("UserText");
NotifyPropertyChanged("IsEnabledCombo");
Bind your control as such
IsEnabled="{Binding IsEnabledCombo }"
So wherever one of the related values can change they also call NotifyPropertyChanged("IsEnabledCombo"); and the control status is updated automatically.
I provide a another notify chained example doing similar on my blog:
Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding
My Problem
I am trying to prevent the user to add empty DataGrid rows when using the built-in .NET DataGrid AddNewItem-functionality. So when a user tries to commit an AddNew transaction of the DataGrid and leaves PageItemViewModel.Text empty, it should disappears from the DataGrid.
The code
ViewModels
public class PageItemViewModel
{
public string Text { get; set; }
}
public class PageViewModel
{
public ObservableCollection<PageItemViewModel> PageItems { get; } = new ObservableCollection<PageItemViewModel>();
}
View
<DataGrid AutoGenerateColumns="True"
CanUserAddRows="True"
ItemsSource="{Binding PageItems}" />
I already tried
... removing the automatically created object from the DataGrid's ItemsSource while handling:
DataGrid.AddingNewItem
INotifyCollectionChanged.CollectionChanged of PageViewModel.PageItems
IEditableCollectionView.CancelNew
DataGrid.OnItemsChanged
... but always receive exceptions like:
"System.InvalidOperationException: 'Removing' is not allowed during an AddNew or EditItem transaction."
"System.InvalidOperationException: Cannot change ObservableCollection during a CollectionChanged event."
"System.InvalidOperationException: Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead."
My question
How can I prevent the newly created PageItemViewModel from being added to the
ObservableCollection<PageItemViewModel>, when there is a given condition (in this case: String.IsNullOrWhiteSpace(PageItemViewModel.Text) == true.
EDIT:
#picnic8: The AddingNewItem event does not provide any form of RoutedEventArgs and therefore no Handled property. Instead, it is AddingNewItemEventArgs. Your code is invalid.
private void DataGrid_AddingNewItem(object sender, AddingNewItemEventArgs e)
{
var viewModel = (PageItemViewModel)e.NewItem;
bool cancelAddingNewItem = String.IsNullOrWhiteSpace(viewModel.Text) == true;
// ??? How can i actually stop it from here?
}
You can't and shouldn't prevent adding to the underlying collection because when the end user starts typing in the new row, DataGrid will create and add a new PageItemViewModel object which at that time is initialized with default values.
What you can do though is to prevent committing that object by handling the DataGrid.RowEditEnding event when DataGridRowEditEndingEventArgs.EditAction is DataGridEditAction.Commit and use DataGrid.CancelEdit method to effectively remove the new object (or restore the existing object state) when the validation failed.
private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
if (e.EditAction == DataGridEditAction.Commit)
{
var bindingGroup = e.Row.BindingGroup;
if (bindingGroup != null && bindingGroup.CommitEdit())
{
var item = (PageItemViewModel)e.Row.Item;
if (string.IsNullOrWhiteSpace(item.Text))
{
e.Cancel = true;
((DataGrid)sender).CancelEdit();
}
}
}
}
An important detail is that the RowEditEnding event is fired before pushing the current editor value to the data source, so you need to do that by hand before performing the validation. I've used BindingGroup.CommitEdit method for that.
In your VM, subscribe to the AddingNewItem event and check your condition there. You can stop the action if the condition fails.
var datagrid.AddingNewItem += HandleOnAddingNewItem;
public void HandleOnAddingNewItem(object sender, RoutedEventArgs e)
{
if(myConditionIsTrue)
{
e.Handled = true; // This will stop the bubbling/tunneling of the event
}
}
This might be a duplicate question, but I'm unable to find a good answer. All the answers like Binding WinForms ListBox to object properties don't work on my WinForm. I'll explain.
I have a list of Firms that I show in a ListBox. I would like when the SelectedItem changes, that it updates a property on my model. So that I can read the Firms properties.
// the classes
public class Firm
{
public string Name { get; set; }
public int Id { get; set; }
// more properties ...
}
public class MyModel : INotifyPropertyChanged
{
private Firm _firm = new Firm();
public Firm Firm
{
get { return _firm; }
set
{
if (Equals(value, _firm)) return;
_firm = value;
OnPropertyChanged();
}
}
// more properties and OnPropertyChanged() ...
}
// the form
private MyModel Model;
public void MyForm(List<Firm> firms)
{
lstFirm.DataBindings.Add("SelectedItem", Model, "Firm",
true, DataSourceUpdateMode.OnPropertyChanged);
lstFirm.DisplayMember = "Name";
lstFirm.ValueMember = "Id";
lstFirm.DataSource = firms;
}
public void lstFirm_SelectedIndexChanged(object sender, EventArgs e)
{
// Do something with Model.Firm
}
The problem is that Model.Firm null is. Does anybody have an idea what I need to do to make a databinding between the ListBox and the Model? I bind other stuff on my WinForm (such as TextBoxes to String properties) and those work nicely.
From what I can see, your code never sets Model.Firm... Where's the constructor for MyModel? If you don't provide one, Model.Firm will stay null unless you explicitly set it. Here's an example constructor:
public MyModel(Firm firm)
{
_firm = firm;
}
Also, Equals() doesn't do what you think it does. Instead of if (Equals(value, _firm)) return;, use this: if (value == _firm) return;
Ok, so after a weekend of testing, I figured it out.
I was debuging in the SelectedIndexChanged event and didn't see the change in my Model.Firm just yet. But as the SelectedItemChanged event is only internal, I couldn't use that and that's where the databinding on SelectedItem applies the values to databound items.
Now the reason why the change isn't visible yet, is because the SelectedItemChanged is only fired after the SelectedIndexChanged is executed. So internally in the ListBox control, it probably looks like
this.SelectedIndex = value;
this.SelectedItem = FindItem(value);
this.SelectedIndexChanged(/*values*/);
this.SelectedItemChanged(/*values*/); // Apply databinding changes
So it's quite normal that you don't see the changes, before the change has occured. And I didn't know this, so I was kinda stumped why the SelectedItem (who was displaying the changed value) wasn't copied over to the databound model property.
So I didn't have to change anything major to get it all working. :)
I have a ListBox that is bound to a BindingList collection. This works great.
My only grief occurs when the first item enters the collection. The default behavior of the ListBox is to select that item - yet, this does not raise the SelectedIndexChanged event. I assume this is because the SelectedIndex is initially set to null; on becoming anything but null, the index isn't actually changed; rather assigned. How can I stop the default behavior of selecting (highlighting) the first initial item added to the ListBox?
If my assumption is wrong, please shed some light?
Update
Here is the core parts of my code thus far.
public UavControlForm()
{
InitializeComponent();
_controlFacade = new UavController.Facade.ControlFacade();
UpdateFlightUavListBox();
}
private void UpdateFlightUavListBox()
{
lsbFlightUavs.DataSource = _controlFacade.GetFlightUavTally();
lsbFlightUavs.DisplayMember = "Name";
}
private static BindingList<FlightUav> _flightUavTally = new BindingList<FlightUav>();
public BindingList<FlightUav> FlightUavTally
{
get { return _flightUavTally; }
}
public void AddFlightUav(double[] latLonAndAlt)
{
FlightUav flightUav = new FlightUav();
flightUav.Latitude = latLonAndAlt[0];
flightUav.Longitude = latLonAndAlt[1];
flightUav.Altitude = latLonAndAlt[2];
_flightUavTally.Add(flightUav);
UtilStk.InjectAircraftIntoStk(flightUav.Name);
flightUav.SetFlightDefaults();
PlayScenario();
}
Update:
So, setting lsbFlightUavs.SelectedIndex = -1 solves the problem. The above method AddFlightUav() is called from a button's OnClick handler in a second form from the main form. How can I call lsbFlightUavs.SelectedIndex = -1 from this second form when the AddFlightUav() method returns? I know I can make the ListBox static, but that seems like bad practice to me.. what would be a more elegant solution?
Using WinForms, I implemented the Singleton pattern. This enabled me to access the ListBox control from my second form.
Form 1
private static UavControlForm _instance = new UavControlForm();
private UavControlForm()
{
InitializeComponent();
}
public static UavControlForm Instance
{
get { return _instance; }
}
public ListBox FlightUavListBox
{
get { return lsbFlightUavs; }
}
Form 2
UavControlForm.Instance.FlightUavListBox.SelectedIndex = -1;
I am trying to use the built in .NET application settings. So for instance I have a User setting of year.
If the end user changes the setting in the program I need to respond by refreshing the data displayed.
I currently have code like below to do this:
Settings.Default.PropertyChanged += SettingsChanged;
//on year change clear out the grid and update the data
private void SettingsChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "year")
{
grdStudentCourseSearch.DataSource = null;
grdStudentCourseSearch.DataMember = null;
UpdateData();
}
}
As you can see their seems to only be an event handler for all the settings and I am having to use e.PropertyName to compare a string to see which property has changed. Is there a better way to do this? Potentially if I change property names later this could be forgotten.
I believe there isn't a better way (using the generated Settings class), due to the implementation of the Settings Class
Consider the generated class code for a simple string setting:
public string Test {
get {
return ((string)(this["Test"]));
}
set {
this["Test"] = value;
}
}
As you can see, it uses the indexer with a string value - you don't have any specialized TestChanged event or some such. The call to OnPropertyChanged is in the indexer setter:
set
{
SettingChangingEventArgs e = new SettingChangingEventArgs(propertyName, base.GetType().FullName, this.SettingsKey, value, false);
this.OnSettingChanging(this, e);
if (!e.Cancel)
{
base[propertyName] = value;
PropertyChangedEventArgs args2 = new PropertyChangedEventArgs(propertyName);
this.OnPropertyChanged(this, args2);
}
}
You could choose to implement settings like this:
class Settings
{
public event EventHandler YearChanged;
private int _year;
public int Year
{
get { return _year; }
set
{
if (_year != value)
{
_year = value;
OnYearChanged(EventArgs.Empty);
}
}
}
protected virtual void OnYearChanged(EventArgs e)
{
if (YearChanged != null)
YearChanged(this, e);
}
}
Then you could register on the YearChanged event.
No, this isn't a good idea. The code is much too brittle. Catch this earlier. At the user interface level for example, whatever code you have that sets the setting value. It can fire the event and you'll know exactly what is getting modified. Or make an intermediate helper class.
In earlier .net framework (3.5 i think) we can use nameof keyword to avoid magic strings so e.PropertyChange == nameof(Year). So you will be warned by compiler when the property identifier changed.
You could just create a variable and assign the current setting to it at runtime, then just update the one variable whenever it's changed, after comparing it to the previous.