C# WPF DataGridTextColumn custom property - c#

I'm trying to bind data to some sort of custom dependency property or attached property in WPF. I've used the following question
and answer- How to create Custom Property for WPF DataGridTextColumn
as a frame of reference but I need to bind to the 'tag' in WPF. We can call it whatever, but I call it a tag for simplicity as I'm using it on some checkboxes and textboxes in other parts of the code. My Class is as follows:
public class TagTextColumns : DependencyObject
{
public static readonly DependencyProperty TagProperty = DependencyProperty.RegisterAttached(
"Tag",
typeof(object),
typeof(DataGridColumn),
new FrameworkPropertyMetadata(null));
public static object GetTag(DependencyObject dependencyObject)
{
return dependencyObject.GetValue(TagProperty);
}
public static void SetTag(DependencyObject dependencyObject, object value)
{
dependencyObject.SetValue(TagProperty, value);
}
}
I'd like to setup my DataGridTextColumn in WPF to something similar to the following:
<DataGridTextColumn Binding="{Binding Customer}" Header="Customer" local:TagTextColumns.Tag="{Binding tkey}"/>
where tkey is a reference in the db. I do all this to calculate subtotals in the last column per row. Right now, I use a DataGridTemplateColumn with a TextBox inside so I can Tag it with tkey
I've also read up on TechNet about these things but there seems to be little pointing to what exactly I want to do here. I'm not even sure it's possible, but it would seem like it would be.
EDIT:
This is the code I'm using to attempt to call the tag on CellEditEndingfiring
private void resultsDg_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
MessageBox.Show(TagTextColumns.GetTag(sender as DataGridTextColumn).ToString());
}
More details about the problem at hand:
I am trying on four fields from the database to dynamically calculate the subtotal for the last column as the user makes changes. I was trying to use Linq to SQL for this, but using System.Data.SqlClient and System.Data was much faster. Maybe someone can show me a better way. The calculation code is below:
private void CalculateAscessorials(string tkey)
{
decimal SplitTerm = Properties.Settings.Default.SplitTerminal;
decimal SplitDrop = Properties.Settings.Default.SplitDrop;
decimal SiteSplit = Properties.Settings.Default.OnSiteSplit;
decimal Trainer = Properties.Settings.Default.TrainerLoad;
decimal WaitTime = Properties.Settings.Default.TerminalWait;
decimal PumpOut = Properties.Settings.Default.PumpOut;
DataRow[] row = resultDetail.Select("tkey = " + tkey);
decimal payRate;
decimal tempLineItem;
Decimal.TryParse(row[0]["PayForLine"].ToString(), out tempLineItem);
Decimal.TryParse(row[0]["PayRate"].ToString(), out payRate);
if (payRate == 0 && tempLineItem > 0) //this change affts if the rate is 0, that is no rate defined, the rate entered becomes the pay rate for that line only for calculations
row[0]["PayRate"] = tempLineItem;
if (!Convert.ToBoolean(row[0]["SplitDrop"]))
{
Decimal.TryParse(row[0]["PayRate"].ToString(), out payRate);
}
else if (Convert.ToBoolean(row[0]["SplitDrop"]))
{
payRate = SplitDrop;
}
//decimal linePay;
// Decimal.TryParse(row[0]["PayForLine"].ToString(), out linePay);
int terms;
Int32.TryParse(row[0]["SplitLoad"].ToString(), out terms);
decimal waits;
Decimal.TryParse(row[0]["WaitTime"].ToString(), out waits);
int pumps;
Int32.TryParse(row[0]["PumpOut"].ToString(), out pumps);
int sites;
Int32.TryParse(row[0]["SiteSplit"].ToString(), out sites);
int trainings;
Int32.TryParse(row[0]["Trainer"].ToString(), out trainings);
row[0]["PayForLine"] =
(SplitTerm * terms)
+ (waits * WaitTime)
+ (pumps * PumpOut)
+ (sites * SiteSplit)
+ (trainings * Trainer)
+ payRate;
}

You can use the MVVM pattern to calculate the subtotals in the ViewModel. Here's a example that should lead you to the right direction. In the constructor of the MainView I create 2 courses and apply students. A course has a property TotalWeight which calculates the total weight of all students. If the weight of a student changes, or a student is added/removed to the course we have to update the value on the UI. This due to INotifyPropertyChanged and ObservableCollection.
If you use LinqToSQL or even better EntityFramework you can get from store. You could than inject it into the ViewModel constructor as the acutal model. Maybe you even want to introduce a MainViewModel for this purpose. I skipped this for simplicity. But you may want to have a look on dependency injection.
XAML:
<DataGrid ItemsSource="{Binding}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid ItemsSource="{Binding Students}"></DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
code behind:
public class CourseViewModel : ViewModelBase
{
#region Fields
private string _name;
private ObservableCollection<StudentViewModel> _students;
#endregion Fields
#region Constructors
public CourseViewModel()
{
_students = new ObservableCollection<StudentViewModel>();
_students.CollectionChanged += _students_CollectionChanged;
}
#endregion Constructors
#region Properties
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged();
}
}
public ObservableCollection<StudentViewModel> Students
{
get
{
return _students;
}
}
public double TotalWeight
{
get
{
return Students.Sum(x => x.Weight);
}
}
#endregion Properties
#region Methods
private void _students_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// add/remove property changed handlers to students
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (StudentViewModel student in e.NewItems)
{
student.PropertyChanged += Student_PropertyChanged;
}
}
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (StudentViewModel student in e.OldItems)
{
student.PropertyChanged -= Student_PropertyChanged;
}
}
//students were added or removed to the course -> inform "listeners" that TotalWeight has changed
OnPropertyChanged(nameof(TotalWeight));
}
private void Student_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//the weight of a student has changed -> inform "listeners" that TotalWeight has changed
if (e.PropertyName == nameof(StudentViewModel.Weight))
{
OnPropertyChanged(nameof(TotalWeight));
}
}
#endregion Methods
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Constructors
public MainWindow()
{
InitializeComponent();
var course1 = new CourseViewModel() { Name = "Course1" };
course1.Students.Add(new StudentViewModel() { Weight = 100, Name = "Mark" });
course1.Students.Add(new StudentViewModel() { Weight = 120, Name = "Olaf" });
course1.Students.Add(new StudentViewModel() { Weight = 111, Name = "Hans" });
var course2 = new CourseViewModel() { Name = "Course2" };
course2.Students.Add(new StudentViewModel() { Weight = 100, Name = "Mark" });
course2.Students.Add(new StudentViewModel() { Weight = 90, Name = "Renate" });
course2.Students.Add(new StudentViewModel() { Weight = 78, Name = "Judy" });
DataContext = new List<CourseViewModel>()
{
course1,
course2
};
}
#endregion Constructors
}
public class StudentViewModel : ViewModelBase
{
#region Fields
private string _name;
private double _weight;
#endregion Fields
#region Properties
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged();
}
}
public double Weight
{
get
{
return _weight;
}
set
{
_weight = value;
OnPropertyChanged();
}
}
#endregion Properties
}

Related

How to update textblock in real-time?

I'm working on project with MVVM Light framework.
I have MainViewModel which helps me to navigate between viewmodels. I have GoBack and GoTo methods. Their are changing CurrentViewModel.
private RelayCommand<string> _goTo;
public RelayCommand<string> GoTo
{
get
{
return _goTo
?? (_goTo = new RelayCommand<string>(view
=>
{
SwitchView(view);
}));
}
}
private void SwitchView(string name)
{
switch (name)
{
case "login":
User = null;
CurrentViewModel = new LoginViewModel();
break;
case "menu":
CurrentViewModel = new MenuViewModel();
break;
case "order":
CurrentViewModel = new OrderViewModel();
break;
}
In MainWindow there is content control and data templates.
[...]
<DataTemplate DataType="{x:Type vm:LoginViewModel}">
<view:Login/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MenuViewModel}">
<view:Menu/>
</DataTemplate>
[...]
<ContentControl VerticalAlignment="Top" HorizontalAlignment="Stretch"
Content="{Binding CurrentViewModel}" IsTabStop="false"/>
In my OrderView (it is UserControl) I have textblock which should shows TotalPrice of order.
<TextBlock Text="{Binding AddOrderView.TotalPrice}" Padding="0 2 0 0" FontSize="20" FontWeight="Bold" HorizontalAlignment="Right"/>
OrderViewModel has a property TotalPrice and it works well. When I am debugging I see that it has been changed, but in my View nothing happend.
private decimal _totalPrice;
public decimal TotalPrice
{
get
{
_totalPrice = 0;
foreach (var item in Products)
{
item.total_price = item.amount * item.price;
_totalPrice += item.price * item.amount;
}
return _totalPrice;
}
set
{
if (_totalPrice == value)
return;
_totalPrice = value;
RaisePropertyChanged("TotalPrice");
}
}
OrderViewModel iherits from BaseViewModel and it implements INotifyPropertyChanged.
Why is my textblock not updating/refreshing? How to do this?
When I change view with back button and go again to OrderView I see changes!
I spend few days to looking for solution and nothing helps me.
https://i.stack.imgur.com/K8lip.gif
So it's look like when View is seting there is no way to change it without reload its. I don't know how it works exactly.
You shouldn't do calculations or any time consuming operations in the getter or setter of a property. This can drastically degrade performance. If the calculations or operation is time consuming, then you should execute it in a background thread and raise the PropertyChangedevent once the Taskhas completed. This way the invocation of the getter or setter of a property won't freeze the UI.
Explanation of the behavior you observed:
The side effect of changing the property value in its own a getter instead of the setter is that the new value won't propagate to the binding targets. The getter is only invoked by a binding when the PropertyChanged event occurred. So doing the calculations in the getter doesn't trigger the binding to refresh. Now when reloading the page, all bindings will initialize the binding target and therefore invoke the property getter.
You have to set the TotalPrice property (and not the backing field) in order to trigger a refresh of the binding target. But as you already experienced yourself, raising the PropertyChanged event of a property in the
same getter will lead to an infinite loop and therefore to a StackOverflowException.
Also the calculation will be always executed whenever the property's getter is accessed - even when the TotalPrice hasn't changed.
The value of TotalPrice depends on Products property. To minimize the occurrence of the calculation of TotalPrice, only calculate when Products has changed:
OrderViewModel.cs
public class OrderViewModel : ViewModelBase
{
private decimal _totalPrice;
public decimal TotalPrice
{
get => this._totalPrice;
set
{
if (this._totalPrice == value)
return;
this._totalPrice = value;
RaisePropertyChanged();
}
}
private ObservableCollection<Product> _products;
public ObservableCollection<Product> Products
{
get => this._products;
set
{
if (this.Products == value)
return;
if (this.Products != null)
{
this.Products.CollectionChanged -= OnCollectionChanged;
UnsubscribeFromItemsPropertyChanged(this.Products);
}
this._products = value;
this.Products.CollectionChanged += OnCollectionChanged;
if (this.Products.Any())
{
SubscribeToItemsPropertyChanged(this.Products);
}
RaisePropertyChanged();
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (!e.Action.Equals(NotifyCollectionChangedAction.Move))
{
UnsubscribeFromItemsPropertyChanged(e.OldItems);
SubscribeToItemsPropertyChanged(e.NewItems);
}
CalculateTotalPrice();
}
private void ProductChanged(object sender, PropertyChangedEventArgs e) => CalculateTotalPrice();
private void SubscribeToItemsPropertyChanged(IList newItems) => newItems?.OfType<INotifyPropertyChanged>().ToList().ForEach((item => item.PropertyChanged += ProductChanged));
private void UnsubscribeFromItemsPropertyChanged(IEnumerable oldItems) => oldItems?.OfType<INotifyPropertyChanged>().ToList().ForEach((item => item.PropertyChanged -= ProductChanged));
private void CalculateTotalPrice() => this.TotalPrice = this.Products.Sum(item => item.total_price);
private void GetProducts()
{
using (var context = new mainEntities())
{
var result = context.product.Include(c => c.brand);
this.Products = new ObservableCollection<Product>(
result.Select(item => new Product(item.name, item.mass, item.ean, item.brand.name, item.price)));
}
}
public void ResetOrder()
{
this.Products
.ToList()
.ForEach(product => product.Reset());
this.TotalPrice = 0;
}
public OrderViewModel()
{
SetView("Dodaj zamówienie");
GetProducts();
}
}
Also make sure that Product (the items in the Products collection) implements INotifyPropertyChanged too. This will ensure that Products.CollectionChanged event is raised when Product properties have changed.
To fix the page switching behavior you have to modify the MainViewModel class:
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
// The page viewmodels
private Dictionary<string, ViewModelBase> PageViewModels { get; set; }
public Stack<string> ViewsQueue;
public MainViewModel()
{
User = new User(1, "login", "name", "surname", 1, 1, 1);
this.PageViewModels = new Dictionary<string, ViewModelBase>()
{
{"login", new LoginViewModel()},
{"menu", new MenuViewModel()},
{"order", new OrderViewModel()},
{"clients", new ClientsViewModel(User)}
};
this.CurrentViewModel = this.PageViewModels["login"];
this.ViewsQueue = new Stack<string>();
this.ViewsQueue.Push("login");
Messenger.Default.Register<NavigateTo>(
this,
(message) =>
{
try
{
ViewsQueue.Push(message.Name);
if (message.user != null) User = message.user;
SwitchView(message.Name);
}
catch (System.InvalidOperationException e)
{
}
});
Messenger.Default.Register<GoBack>(
this,
(message) =>
{
try
{
ViewsQueue.Pop();
SwitchView(ViewsQueue.Peek());
}
catch (System.InvalidOperationException e)
{
}
});
}
public RelayCommand<string> GoTo => new RelayCommand<string>(
viewName =>
{
ViewsQueue.Push(viewName);
SwitchView(viewName);
});
protected void SwitchView(string name)
{
if (this.PageViewModels.TryGetValue(name, out ViewModelBase nextPageViewModel))
{
if (nextPageViewModel is OrderViewModel orderViewModel)
orderViewModel.ResetOrder();
this.CurrentViewModel = nextPageViewModel;
}
}
}
Your modifified Product.cs
public class Product : ViewModelBase
{
public long id { get; set; }
public string name { get; set; }
public decimal mass { get; set; }
public long ean { get; set; }
public long brand_id { get; set; }
public string img_source { get; set; }
public string brand_name { get; set; }
private decimal _price;
public decimal price
{
get => this._price;
set
{
if (this._price == value)
return;
this._price = value;
OnPriceChanged();
RaisePropertyChanged();
}
}
private long _amount;
public long amount
{
get => this._amount;
set
{
if (this._amount == value)
return;
this._amount = value;
OnAmountChanged();
RaisePropertyChanged();
}
}
private decimal _total_price;
public decimal total_price
{
get => this._total_price;
set
{
if (this._total_price == value)
return;
this._total_price = value;
RaisePropertyChanged();
}
}
public Product(long id, string name, decimal mass, long ean, long brandId, decimal price, string imgSource)
{
this.id = id;
this.name = name;
this.mass = mass;
this.ean = ean;
this.brand_id = brandId;
this.price = price;
this.img_source = imgSource;
}
public Product(string name, decimal mass, long ean, string brandName, decimal price)
{
this.id = this.id;
this.name = name;
this.mass = mass;
this.ean = ean;
this.brand_name = brandName;
this.price = price;
}
public void Reset()
{
// Resetting the `amount` will trigger recalculation of `total_price`
this.amount = 0;
}
protected virtual void OnAmountChanged()
{
CalculateTotalPrice();
}
private void OnPriceChanged()
{
CalculateTotalPrice();
}
private void CalculateTotalPrice()
{
this.total_price = this.price * this.amount;
}
}
The issue was that you have always created a new view model when switching to a page. Of course all previous page information gets lost. You have to reuse the same view model instance. To do this, just store them in a dedicated private property that you initialize once in the constructor.
It's not updating because you're only calling RaisePropertyChanged("TotalPrice"); in the setter. Whereas in your getter is the calculation. So anytime you change the Products property or the content of the Products collection, you also need to call RaisePropertyChanged("TotalPrice"); to notify the View that TotalPrice has been updated.
So if you change any of the item.amount or item.price or if you add or remove items from the Products list then you need to also call. RaisePropertyChanged("TotalPrice");
Ex:
Products.Add(item);
RaisePropertyChanged("TotalPrice"); //This will tell you're View to check for the new value from TotalPrice

WPF DataGrid - Notify another class of user selected value

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);

how to use relational tables in a WPF viewmodel

I'm building a N tier WPF app. I want zero codebehind.
Let's say I have 3 normalized related tables to record sales transactions.
TRANSACTIONS:
TransactionId,
ItemId,
SupplierId,
Price
SUPPLIERS:
SupplierId,
SupplierName
ITEMS:
ItemId,
ItemName.
For each table I have a Base class that reflects the fields. Then a data layer that populates a collection of base objects as required.
I want to have a Listbox on the page showing a list of all of the transactions, 1 transaction per row, the rows should look something like this...
"Trainers FootLocker €99"
"Trousers TopShop €45"
"Coat TopShop €49"
If I use the
<ListBox
ItemsSource="{Binding Path=Transactions}"
SelectedItem="{Binding CurrentTransaction}"
then I end up with rows of IDs from the Transactions table and not the Name values from the Items and Suppliers tables.
Given that I have collection of transactions filled with only IDs to the other tables, what is the best approach to populating the listbox?
One thing I'm wondering is, should my Transactions Base object contain Item item populated there instead of int ItemId?
Transaction Base Model:
using System;
using System.ComponentModel;
using PFT;
using PFT.Data;
namespace PFT.Base
{
public class Transaction : INotifyPropertyChanged
{
public int Id { get; set; }
private int _itemId;
public int ItemId
{
get { return _itemId; }
set {
_itemId = value;
ItemData id = new ItemData();
this.Item = id.Select(value);
NotifyPropertyChanged("ItemId");
}
}
private Item _item;
public Item Item
{
get { return _item; }
set { _item = value; }
}
private float _price;
public float Price
{
get { return _price; }
set {
_price = value;
NotifyPropertyChanged("Price");
}
}
private DateTime _date;
public DateTime Date
{
get { return _date; }
set {
_date = value;
NotifyPropertyChanged("Date");
}
}
private string _comment;
public string Comment
{
get { return _comment; }
set
{
_comment = value;
NotifyPropertyChanged("Comment");
}
}
private int _traderId;
public int TraderId
{
get { return _traderId; }
set
{
_traderId = value;
NotifyPropertyChanged("TraderId");
}
}
private Trader _trader;
public Trader Trader
{
get { return _trader; }
set { _trader = value;
TraderData t = new TraderData();
this.Trader = t.Select(value);
}
}
private string _insertType;
/// <summary>
/// A - Auto, M - Manual, V - Verified
/// </summary>
public string InsertType
{
get { return _insertType; }
set { _insertType = value;
NotifyPropertyChanged("InsertType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
//private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
ITEMS BASE CLASS
using System.ComponentModel;
namespace PFT.Base
{
public class Item : INotifyPropertyChanged
{
private int _id;
public int Id
{
get { return _id; }
set { _id = value;
NotifyPropertyChanged("Id");
}
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value;
NotifyPropertyChanged("Name");
}
}
private string _description;
public string Description
{
get { return _description; }
set { _description = value;
NotifyPropertyChanged("Description");
}
}
private float _defaultPrice;
public float DefaultPrice
{
get { return _defaultPrice; }
set { _defaultPrice = value;
NotifyPropertyChanged("DefaultPrice");
}
}
private bool _isIncome;
public bool IsIncome
{
get { return _isIncome; }
set { _isIncome = value;
NotifyPropertyChanged("IsIncome");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The way you'd do this with viewmodels would be to give Transaction a Supplier property and an Item property. Those properties would be references to the actual Item and Supplier objects in their own collections. If the relationship is one ItemID and one SupplierID per transaction, that's the object equivalent. If a transaction can be multiple records with the same transaction ID and different supplier or item IDs, then Transaction needs collections of Item and Supplier. We can do that in WPF too, but it'll take a lot more angle brackets than the trivial example below.
You would set that up when you get your items from the database (however you're doing that), or maybe Entity Framework can do that for you.
Real simple listbox displaying item names: Add DisplayMemberPath.
<ListBox
ItemsSource="{Binding Transactions}"
SelectedItem="{Binding CurrentTransaction}"
DisplayMemberPath="Item.Name"
/>
More complicated:
<ListBox
ItemsSource="{Binding Transactions}"
SelectedItem="{Binding CurrentTransaction}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Item.Name, Mode=OneWay}" />
<Run Text=" - " />
<Run Text="{Binding Supplier.Name, Mode=OneWay}" />
<Run Text=" " />
<Run Text="{Binding Price, Mode=OneWay, StringFormat=c}" />
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You could also look into a columned control like a ListView or DataGrid.
Slightly off topic, zero code-behind is a bit extreme. It's a last resort, not a third rail. Minimal code-behind is a sound general principle. Don't go crazy trying to avoid it; it's there for a reason.
UPDATE
public class Transaction : INotifyPropertyChanged
{
// ... stuff ...
public Item Item
{
get { return _item; }
set {
_item = value;
// If this property is ever set outside the Transaction
// constructor, you ABSOLUTELY MUST raise PropertyChanged here.
// Otherwise, make the setter private. But just raise the event.
// This has nothing whatsoever to do with when or whether the Item
// class raises PropertyChanged, because this is not a property of the
// Item class. This is a property of Transaction.
NotifyPropertyChanged("Item");
}
}
// ... more stuff ...

set property from running thread to ViewModel in Caliburn Micro

I'm beginner in Caliburn.Micro so if something is not clear or this implementation is wrong, please let me know. I have a DataGridViewModel with properties Name, Data
class DataGridViewModel : Screen
{
private char name;
public char Name
{
get { return name; }
set
{
name = value;
this.NotifyOfPropertyChange(() => Name);
}
}
private DataTable data;
public DataTable Data
{
get { return data; }
set
{
data = value;
this.NotifyOfPropertyChange(() => Data);
}
}
}
and a MainViewModel with properties Fitness and GenerationCout. In a MainView is a button which starts a new thread of my Genetic class.
public class MainViewModel : Conductor<Screen>.Collection.AllActive
{
private Genetic genetic;
private double fitness;
public double Fitness
{
get { return fitness; }
set
{
fitness = value;
NotifyOfPropertyChange(() => Fitness);
}
}
private int generationCout;
public int GenerationCout
{
get { return generationCout; }
set
{
generationCout = value;
NotifyOfPropertyChange(() => GenerationCout);
}
}
public void Start()
{
List<string> features = new List<string> { "(a*b)*c=a*(b*c)" };
genetic = new Genetic(features);
genetic.Start();
}
public MainViewModel()
{
generationCout = 0;
fitness = 0;
}
}
There are fields like best and generationCount. And there is a method SetActual() which is using in running thread of this class. In the method I'm setting actual fields value and I want to sent it to ViewModels eventually refresh it in Views.
public class Genetic : BaseThread
{
private int generationCount;
private Subject best;
private void SetActual()
{
DataTable data;
char name;
double fitness;
foreach (Operation operation in best.Operations)
{
DataTable data;
data = Converter.ArrayToDataTable(operation);
name = operation.Name;
fitness = best.Fitness;
}
generationCount++;
}
}
So I need show an actual value of those fields in my views during thread is running. Can anyone tell me how to do that with use the right approach?
You can easily notify Genetic's properties change from Genetic class itself by extend PropertyChangedBase and expose it directly from your ViewModel then refer to Genetic class property on MainViweModel from control
public class MainViewModel : Conductor<Screen>.Collection.AllActive
{
private Genetic genetic;
public Genetic CurrentGenetic
{
get { return genetic; }
set
{
genetic = value;
NotifyOfPropertyChange(nameof(CurrentGenetic));
}
}
}
public class Genetic : PropertyChangedBase
{
private int generationCount;
public int GenerationCout
{
get { return generationCount; }
set
{
generationCount = value;
NotifyOfPropertyChange(nameof(CurrentGenetic));
}
}
}
// example binding
<TextBlock Text="{Binding Path=CurrentGenetic.GenerationCout }" />
// or caliburn micro convention
<TextBlock x:Name="CurrentGenetic_GenerationCout" />
if you don't want to extend PropertyChangedBase, you can implement INotifyPropertyChangedEx(caliburn micro versioin of INotifyPropertyChanged) as https://github.com/Caliburn-Micro/Caliburn.Micro/blob/master/src/Caliburn.Micro/PropertyChangedBase.cs
more about Conventions see All about Conventions

C# custom listbox GUI

I have a list of classes, but different children have different properties that need to be displayed.
What I want to achieve is to have a listbox-type control in the gui which enables each child to display it's properties the way it wants to - so not using the same pre-defined columns for every class.
I envisage something like the transmission interface (below), where each class can paint it's own entry, showing some text, progress bar if relevant, etc.
How can this be achieved in C#?
Thanks for any help.
Let your list items implement an interface that provides everything needed for the display:
public interface IDisplayItem
{
event System.ComponentModel.ProgressChangedEventHandler ProgressChanged;
string Subject { get; }
string Description { get; }
// Provide everything you need for the display here
}
The transmission objects should not display themselves. You should not mix domain logic (business logic) and display logic.
Customized ListBox:
In order to do display listbox items your own way, you will have to derive your own listbox control from System.Windows.Forms.ListBox. Set the DrawMode property of your listbox to DrawMode.OwnerDrawFixed or DrawMode.OwnerDrawVariable (if the items are not of the same size) in the constructor. If you use OwnerDrawVariable then you will have to override OnMeasureItem as well, in order to tell the listbox the size of each item.
public class TransmissionListBox : ListBox
{
public TransmissionListBox()
{
this.DrawMode = DrawMode.OwnerDrawFixed;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
e.DrawBackground();
if (e.Index >= 0 && e.Index < Items.Count) {
var displayItem = Items[e.Index] as IDisplayItem;
TextRenderer.DrawText(e.Graphics, displayItem.Subject, e.Font, ...);
e.Graphics.DrawIcon(...);
// and so on
}
e.DrawFocusRectangle();
}
}
You can let your original transmission class implement IDisplayItem or create a special class for this purpose. You can also have different types of objects in the list, as long as they implement the interface. The point is, that the display logic itself is in the control, the transmission class (or whatever class) only provides the information required.
Example:
Because of the ongoing discussion with Mark, I have decided to include a full example here. Let's define a model class:
public class Address : INotifyPropertyChanged
{
private string _Name;
public string Name
{
get { return _Name; }
set
{
if (_Name != value) {
_Name = value;
OnPropertyChanged("Name");
}
}
}
private string _City;
public string City
{
get { return _City; }
set
{
if (_City != value) {
_City = value;
OnPropertyChanged("City");
OnPropertyChanged("CityZip");
}
}
}
private int? _Zip;
public int? Zip
{
get { return _Zip; }
set
{
if (_Zip != value) {
_Zip = value;
OnPropertyChanged("Zip");
OnPropertyChanged("CityZip");
}
}
}
public string CityZip { get { return Zip.ToString() + " " + City; } }
public override string ToString()
{
return Name + "," + CityZip;
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
Here is a custom ListBox:
public class AddressListBox : ListBox
{
public AddressListBox()
{
DrawMode = DrawMode.OwnerDrawFixed;
ItemHeight = 18;
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
const TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
if (e.Index >= 0) {
e.DrawBackground();
e.Graphics.DrawRectangle(Pens.Red, 2, e.Bounds.Y + 2, 14, 14); // Simulate an icon.
var textRect = e.Bounds;
textRect.X += 20;
textRect.Width -= 20;
string itemText = DesignMode ? "AddressListBox" : Items[e.Index].ToString();
TextRenderer.DrawText(e.Graphics, itemText, e.Font, textRect, e.ForeColor, flags);
e.DrawFocusRectangle();
}
}
}
On a form, we place this AddressListBox and a button. In the form, we place some initializing code and some button code, which changes our addresses. We do this in order to see, if our listbox is updated automatically:
public partial class frmAddress : Form
{
BindingList<Address> _addressBindingList;
public frmAddress()
{
InitializeComponent();
_addressBindingList = new BindingList<Address>();
_addressBindingList.Add(new Address { Name = "Müller" });
_addressBindingList.Add(new Address { Name = "Aebi" });
lstAddress.DataSource = _addressBindingList;
}
private void btnChangeCity_Click(object sender, EventArgs e)
{
_addressBindingList[0].City = "Zürich";
_addressBindingList[1].City = "Burgdorf";
}
}
When the button is clicked, the items in the AddressListBox are updated automatically. Note that only the DataSource of the listbox is defined. The DataMember and ValueMember remain empty.
yes, if you use WPF it is quite easy to do this. All you have to do is make a different DataTemplate for your different types.
MSDN for data templates
Dr. WPF for Items Control & Data Templates

Categories

Resources