Related
I am attempting to make an undo system, where when a property on an object that implements INotifyPropertyChanged is changed, the property name and its old value is pushed onto a stack via a KeyValuePair. When the user clicks "Undo" it then pops from the stack and uses reflection to set the property's value to its old value.
The problem with this is that it calls OnPropertyChanged again, so the property and its restored value is added to the undo stack a second time. On the other hand, I still want it to call OnPropertyChanged since I want the view to update its bindings.
There's obviously something wrong with how I'm designing it, but I can't seem to figure out another way of going about it.
Here's my model
internal class MyModel : INotifyPropertyChangedExtended
{
private string testProperty1 = "";
public string TestProperty1
{
get { return testProperty1; }
set {
var oldValue = testProperty1;
testProperty1 = value;
OnPropertyChanged(nameof(TestProperty1), oldValue);
}
}
private string testProperty2 = "";
public string TestProperty2
{
get { return testProperty2; }
set {
var oldValue = testProperty2;
testProperty2 = value;
OnPropertyChanged(nameof(TestProperty2), oldValue);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
}
Here's my INotifyPropertyChangedExtended interface
public class PropertyChangedEventArgsExtended : PropertyChangedEventArgs
{
public virtual object OldValue { get; private set; }
public PropertyChangedEventArgsExtended(string propertyName, object oldValue)
: base(propertyName)
{
OldValue = oldValue;
}
}
public class INotifyPropertyChangedExtended : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName, object oldValue)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgsExtended(propertyName, oldValue));
}
}
And here's my view model
internal class MyViewModel
{
public MyModel MyModel { get; set; } = new();
public Stack<KeyValuePair<string, object>> PropertyStateStack = new();
public RelayCommand Undo { get; set; }
public MyViewModel()
{
SetupCommands();
MyModel.PropertyChanged += MyModel_PropertyChanged;
}
private void MyModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var args = e as PropertyChangedEventArgsExtended;
if (args.OldValue != null)
{
PropertyStateStack.Push(new KeyValuePair<string, object>(args.PropertyName, args.OldValue));
}
}
private void SetupCommands()
{
Undo = new RelayCommand(o =>
{
KeyValuePair<string, object> propertyState = PropertyStateStack.Pop();
PropertyInfo? property = MyModel.GetType().GetProperty(propertyState.Key);
if (property != null)
{
property.SetValue(MyModel, Convert.ChangeType(propertyState.Value, property.PropertyType), null);
}
});
}
}
EDIT: I did research the "memento pattern" but I couldn't get it to work with INotifyPropertyChanged, since as soon as I set MyModel to a backup of it the bindings to the view stopped working.
Implementing Memento or a variant is the right way. Opposed to storing the particular modifying undo action e.g., Action<T> (another good solution), Memento has a higher memory footprint (as it stores the complete object state), but allows random access to the stored states.
The key point is that when implementing Memento properly, you don't have to rely on reflection, which will only make your code slow and heavy.
The following example uses the IEditableObject interface to implement the Memento pattern (variant). The implementation supports undo and redo. The TextBox class is implementing undo/redo in a similar way using the same interface. The advantage is that you have full control over when to record the object's state. You can even cancel the ongoing modification.
This example clones the complete object to backup the state. Because objects can be quite expensive, for example when they allocate resources, it could make sense to introduce an immutable data model that actually stores the values of the public editable properties. Now, instead of cloning the complete object you would only clone the immutable data model. This can improve the performance in critical scenarios.
See the example provided by the IEditableObject link above to learn how to introduce an immutable data model that holds the object's data.
The actual undo/redo logic is encapsulated in the example's abstract StateTracker<TStateObject> class. StateTracker<TStateObject> implements the aforementioned IEditableObject and the ICloneable interface. To add convenience, StateTracker<TStateObject> also implements a custom IUndoable interface (to enable anonymous usage of the public undo/redo API).
Every class that needs to support state tracking (undo/redo) must extend the abstract StateTracker<TStateObject> to provide a ICloneable.Clone and a StateTracker.UpdateState implementation.
The following example is very basic. It allows undo and redo, but does not support random access to undo/redo states. You would have to use an index based backing store like List<T> to implement such a feature.
IUndoable.cs
Enable anonymous access to the undo/redo API.
public interface IUndoable
{
bool TryUndo();
bool TryRedo();
}
StateTracker.cs
Encapsulates the actual undo/redo logic to avoid duplicate implementations
for each type that is supposed to support undo/redo.
You can consider to add a public UndoCommand and RedoCommand to this class and let the commands invoke TryUndo and TryRedo respectively.
public abstract class StateTracker<TStateObject> : IEditableObject, IUndoable, ICloneable
{
public bool IsInEditMode { get; private set; }
private Stack<TStateObject> UndoMemory { get; }
private Stack<TStateObject> RedoMemory { get; }
private TStateObject StateBeforeEdit { get; set; }
private bool IsUpdatingState { get; set; }
protected StateTracker()
{
this.UndoMemory = new Stack<TStateObject>();
this.RedoMemory = new Stack<TStateObject>();
}
public abstract TStateObject Clone();
protected abstract void UpdateState(TStateObject state);
object ICloneable.Clone() => Clone();
public bool TryUndo()
{
if (!this.UndoMemory.TryPop(out TStateObject previousState))
{
return false;
}
this.IsUpdatingState = true;
this.StateBeforeEdit = Clone();
this.RedoMemory.Push(this.StateBeforeEdit);
UpdateState(previousState);
this.IsUpdatingState = false;
return true;
}
public bool TryRedo()
{
if (!this.RedoMemory.TryPop(out TStateObject nextState))
{
return false;
}
this.IsUpdatingState = true;
this.StateBeforeEdit = Clone();
this.UndoMemory.Push(this.StateBeforeEdit);
UpdateState(nextState);
this.IsUpdatingState = false;
return true;
}
// Start recording the changes
public void BeginEdit()
{
if (this.IsInEditMode || this.IsUpdatingState)
{
return;
}
this.IsInEditMode = true;
// Create the snapshot before the instance is changed
this.StateBeforeEdit = Clone();
}
// Abort recording the changes
public void CancelEdit()
{
if (!this.IsInEditMode)
{
return;
}
// Restore the original state
UpdateState(this.StateBeforeEdit);
this.IsInEditMode = false;
}
// Commit recorded changes
public void EndEdit()
{
if (!this.IsInEditMode || this.IsUpdatingState)
{
return;
}
// Commit the snapshot of the original state after the instance was changed without cancellation
this.UndoMemory.Push(this.StateBeforeEdit);
this.IsInEditMode = false;
}
}
MyModel.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
// UpdateState() is called by the StateTracker
// which internally guards against the infinite loop
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
this.testProperty1 = value;
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
this.testProperty2 = value;
OnPropertyChanged();
}
}
}
Example
The following example stores the state of a TextBox, that binds to a MyModel instance. When the TextBox receives focus, the MyModel.BeginEdit method is called to start recording the input. When the TextBox loses focus the recorded state is pushed onto the undo stack by calling the MyModel.EndEdit method.
MainWindow.xaml
<Window>
<Window.DataContext>
<local:MyModel />
</Window.DataContext>
<StackPanel>
<Button Content="Undo"
Click="OnUndoButtonClick" />
<Button Content="Redo"
Click="OnRedoButtonClick" />
<TextBox Text="{Binding TestProperty1, UpdateSourceTrigger=PropertyChanged}"
GotFocus="OnTextBoxGotFocus"
LostFocus="OnTextBoxLostFocus" />
</StackPanel>
</Window>
MainWindow.xaml.cs
Because of the defined interfaces we can handle undo/redo without knowing the actual data type.
private void OnTextBoxGotFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).BeginEdit();
private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
=> ((sender as FrameworkElement).DataContext as IEditableObject).EndEdit();
private void OnUndoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryUndo();
private void OnRedoButtonClick(object sender, RoutedEventArgs e)
=> _ = ((sender as FrameworkElement).DataContext as IUndoable).TryRedo();
An alternative flow could be that the MyModel class internally calls BeginEdit and EndEdit inside the relevant property setters (before accepting the new value and after accepting the new value). In case of the TextBox, the advantage of this solution is that it allows to record every single input.
In this scenario, the GotFocus and LostFocus event handlers previously defined on the TextBox (example above) are not needed and related code must be removed:
MyModel.cs
public class MyModel : StateTracker<MyModel>, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MyModel()
{
}
// Copy constructor
private MyModel(MyModel originalInstance)
{
// Don't raise PropertyChanged to avoid the loop of death
this.testProperty1 = originalInstance.TestProperty1;
this.testProperty2 = originalInstance.TestProperty2;
}
// Create a deep copy using the copy constructor
public override MyModel Clone()
{
var copyOfInstance = new MyModel(this);
return copyOfInstance;
}
protected override void UpdateState(MyModel state)
{
// UpdateState() is called by the StateTracker
// which internally guards against the infinite loop
this.TestProperty1 = state.TestProperty1;
this.TestProperty2 = state.TestProperty2;
}
private void RecordPropertyChange<TValue>(ref TValue backingField, TValue newValue)
{
BeginEdit();
backingField = newValue;
EndEdit();
}
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private string testProperty1;
public string TestProperty1
{
get => this.testProperty1;
set
{
RecordPropertyChange(ref this.testProperty1, value);
OnPropertyChanged();
}
}
private string testProperty2;
public string TestProperty2
{
get => this.testProperty2;
set
{
RecordPropertyChange(ref this.testProperty2, value);
OnPropertyChanged();
}
}
}
Remarks
If extending StateTracker is not an option (e.g., because it would introduce a multi-inheritance issue), you can always make use of composition (for example add a private property of type StateTracker to your undoable model to replace inheritance).
Just create a new class that extends StateTracker to implement the abstract members. Then define a private property of this new type in your undoable model. Now, let the model reference this private property to access the undo/redo API.
While composition is to be favored, this example chooses inheritance as this concept feels more natural to most. It may helps to understand the basic idea.
I have been trying this for a few days now. I am new to coding and quite new to WPF and DataGrids. Any help is greatly appreciated.
See Image of the datagrid
Basically the datagrid on the left has a different ItemSource to the one on the right.
In the right datagrid, the user selects the brand for which he wants to calculate the total amount. I capture the selected row and get the brand rate as follows -
private void BrandGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
List<DataGridCellInfo> info = BrandGrid.SelectedCells.ToList();
rate = 0;
if (info.Count > 0)
{
Brand i = (Brand)info[1].Item;
rate = i.Rate;
}
}
Now how do I tell the other class to use the selected rate to calculate the total amount. In the same fashion as I calculate the total area
TotalArea = length * quantity * someConstantWidth (this one was easy using INotifyPropertyChanged). When the user edited the length and quantity in the datagrid, the TotalArea would update simultaneously.
Whereas here TotalAmount = TotalArea * SelectedRate has gotten me quite confused as to how to send the information from the MainWindow.BrandGrid_SelectedCellsChanged to another class.
I have tried poking around with delegate and events unsuccessfully cause in my example application I'm finding it difficult on how to implement it.
What is the best method? How do I go on about this?
UPDATE
Thanks you so much for such a detailed response! I have few more doubts regarding the implementation ,
(1) In step 1 the line of code: public void SelectBrand() => OnBrandSelect?.Invoke(this, Rate);
I assume here Rate is the variable name and not the type? My variable Rate is a double type.
(2) To address your note on the Result Class: The length and quantity are user inputs - using which Total Area is calculated. Here Length, Quantity, Area etc are within the same class Result and I use INotifyPropertyChanged to update the TotalArea column.
(3) In your example for Result Class, it requires me to create the ResultObject with a Brand Type input. Any other way? As user should be able to include all the length and quantities of an order placed by a customer and then select the brand later.
I would like the output to be similar to how INotifyPropertyChanged handles changes. It changes the cells real time as I am changing the inputs in the other cells - similar behavior if possible would be awesome.
UPDATE 2
B.Spangenberg, you have given an excellent solution. I tried your code and added a button to add items and the code works perfectly but I guess I had some requirements missing from my question.
The DataGrid I have is "IsEnabled" and allows a new row to be added - once the present row is added, the new row automatically appears which can be edited.
By understanding your solution, here are the proper requirements -
(1) No button to add items to the OrderGrid. New row appears automatically.
(2) The brand can be selected first before entering the items or after entering the items.
(3) Once the user selects a brand ALL the items' TotalAmount are updated. There is NO selection of an item in the OrderGrid.
(4) If the user adds a new item in the OrderGrid after selecting a brand, the new item's TotalAmount is calculated based on the brand that is already presently selected.
Note: You are right about "SelectedCellsChanged" I have changed that to a MouseDoubleClick event. It's now very reliable and also a better feel for the user. THANK YOU!
So, I assume you're trying to invoke an event from your selected brand to convey to anyone listing that the brand rate is (), which is then used to calculate the Total.
Step 1:
You'll need to add an event to your "Brand" class and create a public method to invoke it internally.
class Brand
{
public string Name { get; set; }
public Rate Rate { get; set; }
public void SelectBrand() => OnBrandSelect?.Invoke(this, Rate);
public event EventHandler<Rate> OnBrandSelect;
}
Step 2:
Hook your brand events when you load them into the collection that contains them which is used as the grid source. Basic example below.
// load brands from db or elswhere.
List<Brand> brandsSelectedToLoad = new List<Brand>()
ObservableCollection<Brand> BrandDisplayCollectionToBeUsedAsGridSource = new ObservableCollection<Brand>();
foreach (Brand brand in brandsSelectedToLoad)
{
brand.OnBrandSelect += (s, args) => { /* Call calculation method (TotalAmount = TotalArea * args -- args is rate) and set the result */}
BrandDisplayCollectionToBeUsedAsGridSource.Add(brand);
}
Step 3:
Then, when you select your brand from the grid, you invoke the brand event. Any listeners should then do their jobs.
private void BrandGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
List<DataGridCellInfo> info = BrandGrid.SelectedCells.ToList();
if (info.Count > 0)
{
Brand selectedBrand = info[1].Item as Brand;
if (selectedBrand == null)
return;
selectedBrand.SelectBrand();
}
}
NOTE :
The other grid which I'll refer to as the "Result Grid" will most likely need to reflect these calculations based on rate invoked by the event, within the Total Amount column.
I assume these items in your "Result Grid" are actual result objects,
but I don't see any clear relation between the result record and the
selected brand. (unless it's hidden within the result object). Which
leaves me to believe that you just add the record to the Result Grid on brand select and forget it.
You will need to embed the same Brand object reference that you load into you Brands Grid into your result object and hook it's event inside of the result object. (Almost the same as in step step two). You can ignore step two then and use the below.
Example :
class Result : INotifyPropertyChanged
{
private double total;
public Result(Brand brandRef)
{
this.Brand = brandRef;
brand.OnBrandSelect += (s, args) =>
{
/* Call calculation method (TotalAmount = TotalArea *
args -- args is rate) and set the result */
Total = /*(TotalAmount = TotalArea *
args -- args is rate)*/;
}
}
public double Total
{
get { return total; }
set
{
total = value;
NotifyPropertyChanged();
}
}
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Update Response
The rate object can be replace by your double type. Just swap all "Rate" with double.
I see. So the brand selection is only available after the user has configured the length and quantity inputs. Therefore having the brand parameter within the construct is not possible, cause you need your result object instantiated beforehand.
Do the following :
1) Create brand object as I have it in step one. Just replace the Rate with double.
2) Add a method called "Calculate(double rate)" to your result object which takes double. Within this method you run you calculation for total and set the total property.
public void Caculate(double rate)
{
TotalAmount = TotalArea * rate;
}
3) So now you need some way to keep you event subscribed to the selected grid row on the result grid. So each time you add a result object into your grid source collection, you will need to hook each brand object event to it.
So I assume somewhere you have a button that adds a new record to the result grid to be configured by the user and then he selects the brand. Just before the point where you add the object to the result object collect. Hook it with the following.
ResultObject resultObj = new ResultObject()
BrandCollection.All(brand =>
{
brand.OnBrandSelect += (s, rate) =>
{
resultObj.Calculate(rate);
}; return true;
});
resultGridCollection.Add(resultObj);
So now each brand object has a hooked function that calculates and sets the existing result object on select. You still use Step 3 to invoke the selection event.
Keep in mind you will need to unsubscribe all brand events every time you add a new object, otherwise the brand selection will keep altering all your results. If you want to support changes on selected GridResult instead, you just need to move the subscription code to from the add method to the selected grid cell method for result.
I hope this all makes sense.
Update 2
I decided to go the extra mile for you. Here is the code you need. All of it.
namespace GridSelection
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
MainWindowViewModel model = new MainWindowViewModel();
public MainWindow()
{
InitializeComponent();
OrdersGrid.SelectedCellsChanged += OrdersGrid_SelectedCellsChanged;
BrandGrid.SelectedCellsChanged += BrandGrid_SelectedCellsChanged;
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
model.Load();
this.DataContext = model;
}
private void OrdersGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
DataGrid grid = sender as DataGrid;
Order selectedOrder = grid.SelectedItem as Order;
if (selectedOrder == null)
return;
model.BrandCollection.All(b =>
{
b.UnsubscribeAll();
b.OnBrandSelect += (s, rate) =>
{
selectedOrder.CalculateTotal(rate);
}; return true;
});
}
private void BrandGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
{
DataGrid grid = sender as DataGrid;
Brand selectedbrand = grid.SelectedItem as Brand;
if (selectedbrand == null)
return;
selectedbrand.InvokeBrandSelected();
}
}
internal class MainWindowViewModel
{
public ObservableCollection<Brand> BrandCollection { get; private set; }
public ObservableCollection<Order> OrderCollection { get; private set; }
public ICommand AddNewOrderCommand { get; private set; }
public void Load()
{
BrandCollection = new ObservableCollection<Brand>
{
new Brand() { Name = "Brand One", Rate = 20 },
new Brand() { Name = "Brand Two", Rate = 30 },
new Brand() { Name = "Brand Three", Rate = 50 }
};
OrderCollection = new ObservableCollection<Order>();
AddNewOrderCommand = new CustomCommand(p =>
{
OrderCollection.Add(new Order());
});
}
}
public class Order : INotifyPropertyChanged
{
#region Private Variables
private int length;
private int quantity;
private int totalArea;
private double totalAmount;
#endregion
#region Public Properties
public int Length
{
get { return length; }
set
{
length = value;
NotifyPropertyChanged();
CalculateArea();
}
}
public int Quantity
{
get { return quantity; }
set
{
quantity = value;
NotifyPropertyChanged();
CalculateArea();
}
}
public int TotalArea
{
get { return totalArea; }
set
{
totalArea = value;
NotifyPropertyChanged();
}
}
public double TotalAmount
{
get { return totalAmount; }
set
{
totalAmount = value;
NotifyPropertyChanged();
}
}
#endregion
private void CalculateArea()
{
TotalArea = this.Length * this.Quantity;
}
public void CalculateTotal(double rate)
{
TotalAmount = this.TotalArea * rate;
}
#region Public Methods
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class Brand
{
public string Name { get; set; }
public double Rate { get; set; }
public void InvokeBrandSelected() => OnBrandSelect?.Invoke(this, Rate);
public void UnsubscribeAll() => OnBrandSelect = null;
public event EventHandler<double> OnBrandSelect;
}
// Interface
public interface ICustomCommand : ICommand
{
event EventHandler<object> Executed;
}
// Command Class
public class CustomCommand : ICustomCommand
{
#region Private Fields
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
#endregion
#region Constructor
public CustomCommand(Action<object> execute) : this(execute, null)
{
}
public CustomCommand(Action<object> execute, Func<object, bool> canExecute)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute ?? (x => true);
}
#endregion
#region Public Methods
public bool CanExecute(object parameter)
{
return _canExecute(parameter);
}
public void Execute(object parameter = null)
{
Refresh();
_execute(parameter);
Executed?.Invoke(this, parameter);
Refresh();
}
public void Refresh()
{
CommandManager.InvalidateRequerySuggested();
}
#endregion
#region Events
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public event EventHandler<object> Executed;
#endregion
}
}
Just to note, I noticed that the on grid select is not very reliable. You will just need to look into that.It seems when the grid gains focus and is first selected, that does not fire the SelectedCellsChanged.
UPDATE
Here is the code based on the update you posted.
namespace GridSelection
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
MainWindowViewModel model = new MainWindowViewModel();
public MainWindow()
{
InitializeComponent();
BrandGrid.MouseDoubleClick += BrandGrid_MouseDown;
OrdersGrid.InitializingNewItem += OrdersGrid_InitializingNewItem; ;
this.Loaded += MainWindow_Loaded;
}
private void OrdersGrid_InitializingNewItem(object sender, InitializingNewItemEventArgs e)
{
Order newOrder = e.NewItem as Order;
if (newOrder == null)
return;
if(model.CurrentBrand != null)
{
newOrder.UpdateRate(model.CurrentBrand.Rate);
}
model.BrandCollection.All(b =>
{
b.OnBrandSelect += (s, rate) =>
{
newOrder.UpdateRate(model.CurrentBrand.Rate);
newOrder.CalculateTotal();
}; return true;
});
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
model.Load();
this.DataContext = model;
}
private void BrandGrid_MouseDown(object sender, MouseButtonEventArgs e)
{
DataGrid grid = sender as DataGrid;
Brand selectedbrand = grid.SelectedItem as Brand;
if (selectedbrand == null)
return;
selectedbrand.InvokeBrandSelected();
}
}
internal class MainWindowViewModel : INotifyPropertyChanged
{
private Brand currentBrand;
public ObservableCollection<Brand> BrandCollection { get; private set; }
public ObservableCollection<Order> OrderCollection { get; private set; }
public Brand CurrentBrand
{
get { return currentBrand; }
set
{
currentBrand = value;
NotifyPropertyChanged();
}
}
public void Load()
{
BrandCollection = new ObservableCollection<Brand>
{
new Brand() { Name = "Brand One", Rate = 20 },
new Brand() { Name = "Brand Two", Rate = 30 },
new Brand() { Name = "Brand Three", Rate = 50 }
};
OrderCollection = new ObservableCollection<Order>();
}
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Order : INotifyPropertyChanged
{
#region Private Variables
private int length;
private int quantity;
private int totalArea;
private double totalAmount;
#endregion
public Order()
{
}
#region Properties
private double Rate { get; private set; }
public int Length
{
get { return length; }
set
{
length = value;
NotifyPropertyChanged();
CalculateArea();
CalculateTotal();
}
}
public int Quantity
{
get { return quantity; }
set
{
quantity = value;
NotifyPropertyChanged();
CalculateArea();
CalculateTotal();
}
}
public int TotalArea
{
get { return totalArea; }
set
{
totalArea = value;
NotifyPropertyChanged();
}
}
public double TotalAmount
{
get { return totalAmount; }
set
{
totalAmount = value;
NotifyPropertyChanged();
}
}
#endregion
#region Methods
private void CalculateArea()
{
TotalArea = this.Length * this.Quantity;
}
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void CalculateTotal()
{
TotalAmount = this.TotalArea * this.Rate;
}
public void UpdateRate(double rate)
{
Rate = rate;
}
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class Brand
{
public string Name { get; set; }
public double Rate { get; set; }
public void InvokeBrandSelected() => OnBrandSelect?.Invoke(this, Rate);
public void UnsubscribeAll() => OnBrandSelect = null;
public event EventHandler<double> OnBrandSelect;
}
}
Writing this for anyone who might expect a different behavior with INotifyPropertyChanged.
I noticed that real-time value change cannot be triggered with -
public void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
By real-time I mean - as you are changing a value in datagrid, another value in a different column, same row changes simultaneously. Whereas in the above code, you need to click on the cell for the value to update.
The best way for this neat behavior is -
public void NotifyPropertyChanged(string property)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
And call this method as NotifyPropertyChanged(yourVariableName);
I've been trying to make binding work for two labels in a WinForm, but I can't seem to figure out what I'm doing wrong. Currently, I'm implementing the INotifyPropertyChanged interface, and rigged it to a couple of properties within a Form. The current classes this affects are SessionForm.cs, the actual form, and Session.cs, the place where I keep all the information of the program. The labels in question, which are not mentioned in either class, are L_No, which holds the numerical reference of the Note in the musical Scale, and L_Note, which holds the visceral Note value (e.g. C, C#, etc.).
Allow me to explain what everything does within the classes. The program is designed to test your scale knowledge by asking you, based on the your chosen scale, what nth note of the scale is. You use the buttons on the form to make your choice.
These choices are recorded within the Session class, which has been edited to make this more succinct. The array of integers holds the indices of the notes in relation to the scale array, which is in the Scale object. For example, a typical Note array may hold these values: {1,3,0,2,6,1,3,...}. By using the array in the Scale object as a reference, these would translate into musical notes (e.g. D, F, C, E, B, D, F,...). The player's choices are stored within an array of NoteData objects.
In SessionForm.cs I'm manipulating that information over time. Each time a choice is or isn't made (depending on whether or not they attempted to guess in time), the value of the two Labels are changed: L_No, and L_Note. These two Labels are manipulated by the variables NoteIndex and LastNote, respectively. When these change in value, NotifyPropertyChanged occurs, and then the Labels should be updated...but they're not doing so.
Now, in the design section of the form, in the Properties window, I set up the Text property of each Label to be bound to their respective variables within the form, and set to update upon Property Change, but nothing seems to be working.
So what am I doing wrong?
Session.cs:
public class Session
{
public struct NoteData
{
public int Note;
public bool Correct;
public int GuessTime;
}
public Scale Scale;
/// <summary>
/// Holds the notes for one game
/// </summary>
public int[] Notes { get; private set; }
public NoteData[] Data { get; private set; }
/// <summary>
/// Creates a Session
/// </summary>
/// <param name="difficulty">The difficult of the session, refer to the Resources Class for determination.</param>
/// <param name="scale_used">The scale to be used. Refer to the Resources Class for determination.</param>
/// <param name="notes">The notes being used within this Session</param>
public Session(Resources.Difficulties difficulty, Scale scale_used, int[] notes)
{
ID = DateTime.Now;
Diff = difficulty;
Scale = scale_used;
Notes = notes;
Data = new NoteData[notes.Length];
internalIndex = 0;
}
/// <summary>
/// Stores Note input for each guessed
/// </summary>
/// <param name="index">The index of the note the player is currently on</param>
/// <param name="correct">Was the guess correct?</param>
/// <param name="remaining_time">How long did it take for them to guess?</param>
public void StoreNoteInput(int index, bool correct, int remaining_time)
{
if (internalIndex < Data.Length)
Data[internalIndex++] = new NoteData(index, remaining_time, correct);
}
}
SessionForm.cs:
public partial class SessionForm : Form, INotifyPropertyChanged
{
public Session curSession { get; private set; }
Resources.Notes last_note;
/// <summary>
/// The note index number in relation to the scale
/// </summary>
public int NoteIndex
{
get
{ return note_index; }
private set
{
if (note_index != value)
{
note_index = value;
NotifyPropertyChanged("NoteIndex");
}
}
}
/// <summary>
/// Represents the previous note being tested
/// </summary>
public Resources.Notes LastNote
{
get
{
return last_note;
}
private set
{
if (last_note != value)
{
last_note = value;
NotifyPropertyChanged("LastNote");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void TickDownTimer_Tick(object sender, EventArgs e)
{
remainingTime -= countingDown ? 1000 : 100;
if (remainingTime == 0)
{
if (countingDown)
{
countingDown = false;
TickDownTimer.Interval = 100;
}
if (curIndex > 0)
{
//you ran out of time on the last note
RecordNoteInput(curIndex - 1, false);
}
NextNote();
}
SetTimerText();
}
private void RecordNoteInput(int index, bool correct)
{
curSession.StoreNoteInput(index, correct, remainingTime);
NoteIndex = curSession.Notes[curIndex - 1];
LastNote = curSession.Scale.Notes[NoteIndex];
L_Note.ForeColor = correct ? Color.Green : Color.Red;
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
UPDATE: Here's the binding code that comes from SessionForm.Designer.cs:
this.sessionFormBindingSource1 = new System.Windows.Forms.BindingSource(this.components);
this.sessionFormBindingSource2 = new System.Windows.Forms.BindingSource(this.components);
this.sessionFormBindingSource = new System.Windows.Forms.BindingSource(this.components);
//
// L_Note
//
this.L_Note.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.sessionFormBindingSource1, "LastNote", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged, "C"));
this.L_Note.Text = " ";
//
// L_No
//
this.L_No.DataBindings.Add(new System.Windows.Forms.Binding("Text", this.sessionFormBindingSource2, "NoteIndex", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged, "1", "N0"));
this.L_No.Text = " ";
The problem is the way you call NotifyPropertyChanged:
NotifyPropertyChanged("note_index");
and
NotifyPropertyChanged("last_note");
Just remove the strings from the calls like this
NotifyPropertyChanged();
and everything should be fine.
Edit: If it's not, then your bindings are not initialized correctly. Prove:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Forms;
namespace Tests
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}
class TestForm : Form, INotifyPropertyChanged
{
public TestForm()
{
var label = new Label { Parent = this, Left = 16, Top = 16, AutoSize = false, BorderStyle = BorderStyle.FixedSingle };
label.DataBindings.Add("Text", this, "NoteIndex");
var timer = new Timer { Interval = 200, Enabled = true };
timer.Tick += (sender, e) => NoteIndex = (NoteIndex + 1) % 10;
}
int note_index;
public int NoteIndex
{
get { return note_index; }
private set
{
if (note_index != value)
{
note_index = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
I have trying to build a simulator of Ingenico POS terminal (iWL220).
The main screen I have a combo-box. Once user enter id and password the combo-box load 6 menus. If User click btn1 then combo-box clear the menu and add another set of menu. If user click btn1 for new loaded menu then again combo-box cleared and load another set of menu so on.
My problem is for each button click (btn1, btn2, btn3, btn4, btn5) I have to code lot of if else statement. Example;
First menu (on combo-box) has 6 sector.
1.SectorA
2.SectorB
3.SectorC
4.SectorD
5.SectorE
6.SectorF
If user choose 1.SectorA then user click btn1. Then btn1 clear the combo-box and loads another set of menu. This time menu (on combo-box) has 3 companies.
1.CompanyA
2.CompanyB
3.CompanyC
This time user choose 1.CompanyA then user click again btn1. Then btn1 clear the combo-box and loads another set of menu. This time menu (on combo-box) has 2 payment option.
1.FullPayment
2.ParitalPayment
Now this time if user click btn1 or btn2 the combo-box visible become false and in main screen there is a label and text box. The text box allows user to enter the subscriber number and press enter (green button).
I already load Ingenico terminal picture as jpeg and top of it I set my buttons.
I gave only small version of my simulation. In my app there are 114 probability that user can choose.
In my app btn1 has 92 probability to be clicked, btn2 has 53 probability to be clicked and so on. After user enters the subscriber number and click green button the my app use wfc services to format the data and send to sql server.
But before user click each combination of button some where in my app I store the btn number as 422. This 422 means, user chose SectorD + CompanyB + ParitalPayment option. So my wfc will know what it is mean 422.
My question is what is the shortest way to construct my button events for this 114 probability case?
I have 4 buttons. Btn1, Btn2, Btn3 and Btn4. Also I have some arrays as it shown below and 1 combo-box.
1.ArrayMain() = {“1.Water”,”2.Air”,”3.Soil”,”4.Fire”}
1.1. ArrayWater() = {“1.Salty”,”2.Fresh”, “3.Contaminated”}
1.1.1.ArraySalty() = {1.”AA”, 2.”BB”, 3.”CC”}
1.1.2.ArrayFresh() = {1.”DD”, 2.”EE”, 3.”FF”}
1.1.3.ArrayContaminated() = {1.”XX”, 2.”YY”, 3.”ZZ”}
1.2 ArrayAir() = {“1.Fresh”, “2.Contaminated”}
1.3 ArraySoil() = {“1.Normal”, “2.Contaminated”}
1.4 ArrayFire() = {“1.Low”,”2.Mid”,”3.High”}
When my app starts, first array values 1.(ArrayMain) fills the comboBox. This comboBox will have 4 values as, “1.Water”, ”2.Air”, ”3.Soil”, ”4.Fire” in it. If user choose “1.Water” than user clicks Btn1. Than btn1 events clears the comboBox and loads 1.1ArrayWater() values into comboBox.
Second time if user chooses “1.Salty” than user clicks again btn1 and this time btn1 events clears the comboBox and loads 1.1.1ArraySalty() values into comboBox.
Third time if user chooses “2.BB” than user clicks Btn2 and sends the information “BB” for calculation.
First you have 5 (more or less) menu item and each time you press any (number) buttons (1 to 9 lilke in pos terminal) pressed than new menu appears on the screen.
Each button at any specific time shall execute some specific action depending on the state of the system. Obviously, if you try to decide the specific action depending on the multitude of different variables, you will create a lot of branching code. Such code is very difficult to write correctly and even more difficult to debug and maintain.
So, what if we encapsulate current action for each possible state(sequence of state) in some specific class (interface):
/// <summary>
/// Represents internal terminal presenter that is used inside IGlobalTerminalPresenter.
/// </summary>
public interface ITerminalPresenter
{
void UpdateUI();
ITerminalPresenter this[Int32 index]
{
get;
}
ITerminalPresenter Do1();
ITerminalPresenter Do2();
ITerminalPresenter Parent
{
get;
set;
}
void Reset();
}
Inside the form we will use field of a similar interface that will encapsulate all changes of the presenter.
/// <summary>
/// Represents terminal presenter that UI can operate upon.
/// </summary>
public interface IGlobalTerminalPresenter
{
void UpdateUI();
void Do1();
void Do2();
Int32 SelectedIndex
{
get;
set;
}
void Reset();
}
Our event handlers will become:
private void comboBox_SelectedIndexChanged(object sender, EventArgs e)
{
var senderComboBox = (ComboBox)sender;
this.globalTerminalPresenter.SelectedIndex = senderComboBox.SelectedIndex;
}
private void button1_Click(object sender, EventArgs e)
{
this.globalTerminalPresenter.Do1();
}
private void button2_Click(object sender, EventArgs e)
{
this.globalTerminalPresenter.Do2();
}
To allow our concrete TerminalPresenters to interoperate with form we will force our form to implement the following interface:
/// <summary>
/// This represents your UI in technology-independent manner
/// </summary>
public interface ITerminalView
{
String Title { get; set; }
String Input { get; set; }
String Output { get; set; }
String Button1_Text { get; set; }
String Button2_Text { get; set; }
IEnumerable<String> SelectionItems { get; set; }
void Clear();
}
public partial class MainForm : Form,
ITerminalView
{
...
#region ITerminalView implementation
public string Title
{
get { return this.Text; }
set { this.Text = value; }
}
public String Button1_Text
{
get { return this.button1.Text; }
set { this.button1.Text = value; }
}
public String Button2_Text
{
get { return this.button2.Text; }
set { this.button2.Text = value; }
}
public string Input
{
get { return this.textBox_Input.Text; }
set { this.textBox_Input.Text = value; }
}
public string Output
{
get { return this.textBox_Output.Text; }
set { this.textBox_Output.Text = value; }
}
public IEnumerable<string> SelectionItems
{
get { return this.comboBox.Items.Cast<String>(); }
set
{
this.comboBox.Items.Clear();
if (value == null)
return;
foreach (var item in value)
{
this.comboBox.Items.Add(item);
}
}
}
public void Clear()
{
this.comboBox.SelectedIndex = -1;
this.Title = String.Empty;
this.Input = String.Empty;
this.Output = String.Empty;
this.SelectionItems = null;
}
#endregion
For now we will create two TerminalPresenters - one to just allow selection of next option through combobox, one that calculates sum of two numbers. Both of them use the same base class.
/// <summary>
/// Base class for all presenters
/// </summary>
public abstract class TerminalPresenterBase : ITerminalPresenter
{
protected ITerminalView view;
public TerminalPresenterBase(ITerminalView view)
{
if (view == null)
throw new ArgumentNullException("view");
this.view = view;
this.Parent = this;
}
public abstract void UpdateUI();
public abstract ITerminalPresenter this[int index]
{
get;
}
public abstract ITerminalPresenter Do1();
public abstract ITerminalPresenter Do2();
public virtual ITerminalPresenter Parent
{
get;
set;
}
public virtual void Reset()
{
this.UpdateUI();
}
}
/// <summary>
/// Presenter whose sole goal is to allow user to select some other option and press next
/// </summary>
public class SelectOptionPresenter : TerminalPresenterBase
{
private IList<KeyValuePair<String, ITerminalPresenter>> options;
private ITerminalPresenter selected;
private String title;
public SelectOptionPresenter(ITerminalView view,
String title,
IList<KeyValuePair<String, ITerminalPresenter>> options)
: base(view)
{
if (options == null)
throw new ArgumentNullException("options");
this.title = title;
this.options = options;
foreach (var item in options)
{
item.Value.Parent = this;
}
}
public override void UpdateUI()
{
this.view.Clear();
this.view.Button1_Text = "Confirm selection";
this.view.Button2_Text = "Go back";
this.view.Title = title;
this.view.SelectionItems = options
.Select(opt => opt.Key);
}
public override ITerminalPresenter this[int index]
{
get
{
this.selected = this.options[index].Value;
return this;
}
}
public override ITerminalPresenter Do1()
{
return this.ConfirmSelection();
}
public override ITerminalPresenter Do2()
{
return this.GoBack();
}
public ITerminalPresenter ConfirmSelection()
{
this.selected.UpdateUI();
return this.selected;
}
public ITerminalPresenter GoBack()
{
this.Parent.UpdateUI();
return this.Parent;
}
}
public enum APlusBState
{
EnterA,
EnterB,
Result
}
public class StepActions
{
public Action UpdateUI { get; set; }
public Func<ITerminalPresenter> Do1 { get; set; }
public Func<ITerminalPresenter> Do2 { get; set; }
}
public class APlusBPresenter : TerminalPresenterBase
{
private Int32 a, b;
private APlusBState state;
private String error = null;
private Dictionary<APlusBState, StepActions> stateActions;
private void InitializeStateActions()
{
this.stateActions = new Dictionary<APlusBState, StepActions>();
this.stateActions.Add(APlusBState.EnterA,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = this.error ?? "Enter A";
this.view.Input = this.a.ToString();
this.view.Button1_Text = "Confirm A";
this.view.Button2_Text = "Exit";
},
Do1 = () => // Confirm A
{
if (!Int32.TryParse(this.view.Input, out this.a))
{
this.error = "A is in incorrect format. Enter A again";
return this;
}
this.error = null;
this.state = APlusBState.EnterB;
return this;
},
Do2 = () => // Exit
{
this.Reset();
return this.Parent;
}
});
this.stateActions.Add(APlusBState.EnterB,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = this.error ?? "Enter B";
this.view.Input = this.b.ToString();
this.view.Button1_Text = "Confirm B";
this.view.Button2_Text = "Back to A";
},
Do1 = () => // Confirm B
{
if (!Int32.TryParse(this.view.Input, out this.b))
{
this.error = "B is in incorrect format. Enter B again";
return this;
}
this.error = null;
this.state = APlusBState.Result;
return this;
},
Do2 = () => // Back to a
{
this.state = APlusBState.EnterA;
return this;
}
});
this.stateActions.Add(APlusBState.Result,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = String.Format("The result of {0} + {1}", this.a, this.b);
this.view.Output = (this.a + this.b).ToString();
this.view.Button1_Text = "Exit";
this.view.Button2_Text = "Back";
},
Do1 = () => // Exit
{
this.Reset();
return this.Parent;
},
Do2 = () => // Back to B
{
this.state = APlusBState.EnterB;
return this;
}
});
}
public APlusBPresenter(ITerminalView view) : base(view)
{
this.InitializeStateActions();
this.Reset();
}
public override void UpdateUI()
{
this.view.Clear();
this.stateActions[this.state].UpdateUI();
}
public override ITerminalPresenter this[int index]
{
get { throw new NotImplementedException(); }
}
public override ITerminalPresenter Do1()
{
var nextPresenter = this.stateActions[this.state].Do1();
nextPresenter.UpdateUI();
return nextPresenter;
}
public override ITerminalPresenter Do2()
{
var nextPresenter = this.stateActions[this.state].Do2();
nextPresenter.UpdateUI();
return nextPresenter;
}
public override void Reset()
{
this.state = APlusBState.EnterA;
this.a = 0;
this.b = 0;
this.error = null;
}
}
/// <summary>
/// Represents terminal presenter to use inside GUI. It handles current ISpecificTerminalPresenter inside itself.
/// </summary>
public class GlobalTerminalPresenter : IGlobalTerminalPresenter
{
#region Fields
private ITerminalPresenter current;
private Int32 selectedIndex;
#endregion
#region Constructors
public GlobalTerminalPresenter(ITerminalPresenter mainPresenter)
{
if (mainPresenter == null)
throw new ArgumentNullException("mainPresenter");
this.current = mainPresenter;
this.UpdateUI();
}
#endregion
public void UpdateUI()
{
this.current.UpdateUI();
}
public void Do1()
{
this.current = this.current.Do1();
}
public void Do2()
{
this.current = this.current.Do2();
}
public Int32 SelectedIndex
{
get
{
return this.selectedIndex;
}
set
{
this.selectedIndex = value;
if (value == -1)
return;
this.current = this.current[value];
}
}
public void Reset()
{
this.current.Reset();
}
}
Then we initialize them in the constructor of our form:
public partial class MainForm : Form,
ITerminalView
{
private IGlobalTerminalPresenter globalTerminalPresenter;
public MainForm()
{
InitializeComponent();
var nextLevelPresenters = new KeyValuePair<String, ITerminalPresenter>[]
{
new KeyValuePair<String, ITerminalPresenter>(
"A plus B",
new APlusBPresenter(this)),
new KeyValuePair<String, ITerminalPresenter>(
"Just empty selector",
new SelectOptionPresenter(this,
"Selector with no selection choices",
Enumerable
.Empty<KeyValuePair<String, ITerminalPresenter>>()
.ToArray()))
};
var topPresenter = new SelectOptionPresenter(this, "Select the option and press the confirm button", nextLevelPresenters);
this.globalTerminalPresenter = new GlobalTerminalPresenter(topPresenter);
}
P.S.1: These code snippets assume that you have form named MainForm that has two buttons - button1, button2, one combobox, two textBoxes - textBox_Input, textBox_Output.
P.S.2: The pattern used is close enough to Model-View-Presenter, just without DataBindings.
P.S.3 You can create more or less generic state machine Presenters if you modify APlusBPresenter code. Or try to shape up ChainXxxx... classes and interfaces.
P.S.4: And sorry for these walls of code. That's probably too much for [SO] format, so I've put ad-hoc proof of concept at GitHub - https://github.com/Podskal/StackOverflow_29870164.git. It is ugly in many aspects, but as it is, it can at least give few ideas about how to implement your own system.
P.S.5: There are a lot of problematic places in this code, so you should very carefully consider how you will build your own system from it.
I am using a poco class for the following screen but im just wondering how I would achieve the move up and down elements of this screen
I am using ObservableCollection to add my items to a mutual list my question is how would I achieve the move up and move down. I no I would need to change the poco class in real time but not sure how I would achieve this
private void AddColumn(object sender, RoutedEventArgs e)
{
if (this.WizardData == null)
return;
if (this.WizardData.ConcreteCustomColumnsProxy == null)
this.WizardData.ConcreteCustomColumnsProxy = new ObservableCollection<CustomColumnsModel>();
this.WizardData.ConcreteCustomColumnsProxy.Add(new CustomColumnsModel() { CustomColumnsDisplayName = txtDsiplayName.Text
, CustomColumnsOrder = 1, CustomColumnsWidth = Convert.ToInt32(txtWdith.Text) });
this.listView1.ItemsSource = this.WizardData.ConcreteCustomColumnsProxy;
this.listView1.UnselectAll();
this.listView1.Items.Refresh();
My Poco class is as follows
public event PropertyChangedEventHandler PropertyChanged;
public const string IdPropertyName = "CustomColumnsID";
private Guid _Id = Guid.Empty;
public Guid CustomColumnsID
{
get { return _Id; }
set
{
if (_Id == value)
return;
_Id = value;
NotifyPropertyChanged(IdPropertyName);
}
}
public string CustomColumnsDisplayName { get; set; }
public int CustomColumnsWidth { get; set; }
public int CustomColumnsOrder { get; set; }
protected void NotifyPropertyChanged(string key)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(key));
}
}
public EnterpriseManagementObject ActualData { get; private set; }
}
You have some sort of DataGrid control. You need to data bind a collection property to the DataGrid.ItemsSource property and a property of the same type as the items in the collection to the DataGrid.SelectedItems property:
<DataGrid ItemsSource="{Binding YourCollectionProperty}"
SelectedItem="{Binding YourItemProperty}" />
With the DataGrid.SelectedItems property data bound to your YourItemProperty, you can set which item is selected in the UI by setting this property. So to move the selected item down one position, you could do something like this:
int selectedIndex = YourCollectionProperty.IndexOf(YourItemProperty);
if (YourCollectionProperty.Count > selectedIndex)
YourItemProperty = YourCollectionProperty.ElementAt(selectedIndex + 1);
So that is how you perform the actions of the 'Move Down' Button and the 'Move Up' Button would work similarly. Then all you would need to do is to hook up some Click or ICommand event handlers.