Listbox DataTemplate to be applied to different models - wpf

I have two tables that contain just one value that is the key, too.
I created a list box to show it and modify it.
Table 1 is TTypes1 and the field is Type1 String
Table 2 is TTypes2 and the field is Type2 String
I've written this DataTemplate:
<DataTemplate x:Key="ListboxItems">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding}" />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<!-- edit to swap with save -->
<Button
Content=""
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="Save"
Visibility="{Binding IsVisible}" />
<!-- Cancel - visible only on edit -->
<Button
Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
<!-- Delete -->
<Button
Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
<!-- Add -->
<Button
Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
</StackPanel>
</Grid>
</DataTemplate>
Here are the list boxes but I'm not able to get it working as I want.
If I leave it like this:
<Label Grid.Column="0" Content="{Binding}" />
I do not see the text but the type TTypes1 or TTypes2.
But if I write:
<Label Grid.Column="0" Content="{Binding Type1}" />
Then I cannot use it on TType2 list box.
Here is where I use it:
<ScrollViewer
Margin="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ListBox
Margin="2"
AlternationCount="2"
BorderThickness="1"
ItemsSource="{Binding TTypes1}"
SelectedIndex="0"
SelectionMode="Single"
ItemTemplate="{StaticResource ListboxItems}"
Style="{StaticResource MahApps.Styles.ListBox.Virtualized}">
</ListBox>
</ScrollViewer>
and the second one is:
<ScrollViewer
Margin="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ListBox
Margin="2"
AlternationCount="2"
BorderThickness="1"
ItemsSource="{Binding TTypes2}"
SelectedIndex="0"
SelectionMode="Single"
ItemTemplate="{StaticResource ListboxItems}"
Style="{StaticResource MahApps.Styles.ListBox.Virtualized}">
</ListBox>
</ScrollViewer>
What am I missing?

Multiple Data Templates
The usual way to handle this is to create one distinct data template per type, e.g. for TType1 and TType2.
<DataTemplate x:Key="ListboxItemsTType1"
DataType="{x:Type local:TType1}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Content="{Binding Type1}" />
<!-- ...other markup. -->
</Grid>
</DataTemplate>
<DataTemplate x:Key="ListboxItemsTType2"
DataType="{x:Type local:TType2}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Content="{Binding Type2}" />
<!-- ...other markup. -->
</Grid>
</DataTemplate>
Reference the specific templates in your ListBoxes. You can also remove the x:Key from the data templates, so they are automatically applied to a matching type in the ListBox. This also works with mixed items in a list.
<ScrollViewer Grid.Row="0"
Margin="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ListBox
ItemTemplate="{StaticResource ListboxItems}"
...
</ListBox>
</ScrollViewer>
<ScrollViewer Grid.Row="1"
Margin="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ListBox
...
ItemTemplate="{StaticResource ListboxItems}"
</ListBox>
</ScrollViewer>
Other Methods
If you really want to keep a single data template, you will have to switch the binding depending on the item type of the object bound as data context. There are multiple ways to achieve this.
Here is an example that uses a converter that converts an object to its type from a related question, copy it. A style for Label will use data triggers to apply the correct binding based on that type.
<local:DataTypeConverter x:Key="DataTypeConverter" />
<DataTemplate x:Key="ListboxItems">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">
<Label.Style>
<Style TargetType="{x:Type Label}"
BasedOn="{StaticResource {x:Type Label}}">
<Setter Property="Content"
Value="{x:Null}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource DataTypeConverter}}"
Value="{x:Type local:TType1}">
<Setter Property="Content"
Value="{Binding Type1}" />
</DataTrigger>
<DataTrigger Binding="{Binding Converter={StaticResource DataTypeConverter}}"
Value="{x:Type local:TType2}">
<Setter Property="Content"
Value="{Binding Type2}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<StackPanel Grid.Column="1"
Orientation="Horizontal">
<!-- edit to swap with save -->
<Button Content=""
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="Save"
Visibility="{Binding IsVisible}" />
<!-- Cancel - visible only on edit -->
<Button Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
<!-- Delete -->
<Button Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
<!-- Add -->
<Button Click="LockUnlock_Click"
Content="{Binding Icon}"
ContentTemplate="{StaticResource IconPanelButton}"
DockPanel.Dock="Right"
Style="{StaticResource MahApps.Styles.Button.Flat}"
ToolTipService.ToolTip="{Binding ToolTip}" />
</StackPanel>
</Grid>
</DataTemplate>
Other options that completely rely on code, but are easier to reuse are:
Create a special value converter that does the same as the triggers, return a binding created in code with a property path that is based on type
Create a custom markup extension that automatically chooses the property path based on type
I do not provide examples on these options as they are complex and heavily depend on your requirements. Furthermore, I recommend the first approach to create multiple data templates, as this is the most favorable from a perspective of maintenance and flexibility in my opinion.

I think it is better to use ItemsControl.ItemTemplateSelector if you like two datatemplates.
First you need one class inherit class "DataTemplateSelector" and override its method to select which datatemplate to use.
public class ModelItemTemplateSelector: DataTemplateSelector
{
public DataTemplate Model1Template { get; set; }
public DataTemplate Model2Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item is Model1)
{
return Model1Template;
}
else if(item is Model2)
{
return Model2Template;
}
return base.SelectTemplate(item, container);
}
}
Then code in xaml is below
<ListBox ItemsSource="{Binding Source}">
<ListBox.ItemTemplateSelector>
<local:ModelItemTemplateSelector Model1Template="{StaticResource Model1Template}" Model2Template="{StaticResource Model2Template}" />
</ListBox.ItemTemplateSelector>
</ListBox>
And the other code:
Two datatemplates
<DataTemplate x:Key="Model1Template" DataType="{x:Type local:Model1}">
<TextBlock Text="{Binding Age}" />
</DataTemplate>
<DataTemplate x:Key="Model2Template" DataType="{x:Type local:Model2}">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
Two types
public class BaseModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Model1 : BaseModel
{
private int age;
public int Age
{
get { return age; }
set
{
age = value;
this.RaisePropertyChanged(nameof(Age));
}
}
}
public class Model2 : BaseModel
{
private string name;
public string Name
{
get { return name; }
set
{
name = value;
this.RaisePropertyChanged(nameof(Name));
}
}
}
Source in vm
private ObservableCollection<BaseModel> source;
public ObservableCollection<BaseModel> Source
{
get { return source; }
set
{
source = value;
this.RaisePropertyChanged(nameof(Source));
}
}

Related

Converted values from related entities are not refreshed after editing the property

I am using .net core 3.1 in VS2019. I have a user control containing a ListView listing a set of items of a price list, each of them has a Cost, Markup (percent), a discount (percent) given by a related entity called Promotion and the SalePrice, FinalPrice and PromoDescription fields computed by converters. For example, FinalPrice equals SalePrice if the item does not have a promotion, or it is (SalePrice - SalePrice*item.Promotion.Discount/100.0) otherwise.
BTW, all of these entities belong to a EF Core model with lazy loading with proxies, and all the collections are ObervableCollection, which are loaded like the following:
var dbset = Context.Promos;
dbset.Load();
return dbset.Local.ToObservableCollection();
Everything works fine except when I change the Markup or the Promotion properties, which lead to recalculating the values provided by the converters and involving none or one related entity of type Promo. Debugging the windows I can see all the changes applied correctly in the underlying view model before and after they are confirmed to the database, however I have to force it to refresh the values as in the following picture.
This is the final composition of the window:
Window
TabControl
TabItem (n)
PriceListUserControl (my user control)
Here it is the model:
Here is the wpf of the user control containing the listview:
<UserControl x:Class="MyApp.View.UserControls.PriceListUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyApp.View.UserControls"
xmlns:localModel="clr-namespace:MyApp.Model"
xmlns:localVm="clr-namespace:MyApp.ViewModel"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
x:Name="UserControlPriceList">
<UserControl.Resources>
<localVm:PriceListItemConverter x:Key="priceListItemConverter"/>
<localVm:DateConverter x:Key="dateConverter"/>
<localVm:PromoConverter x:Key="promoConverter" />
<Border Style="{StaticResource BorderStyle}" Name="bottomBorder">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="106" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Producto:</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Costo:</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Markup (%):</TextBlock>
<TextBlock Grid.Row="3" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Precio de Lista:</TextBlock>
<TextBlock Grid.Row="4" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Promoción:</TextBlock>
<TextBlock Grid.Row="5" Grid.Column="0" Style="{StaticResource SmallTitleStyle}" Margin="5"> Precio Final:</TextBlock>
<TextBlock Name="ProductDescription" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Path=Product.Description}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBlock Name="CostPrice" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Path=Product.CostPrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBox Name="MarkupPriceEntryForm" AutomationProperties.Name="Markup" Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left"
Validation.ErrorTemplate="{StaticResource ValidationTemplate}"
Style="{StaticResource TextStyleTextBox}" Margin="8,5,0,5" IsReadOnly="{Binding ElementName=UserControlPriceList, Path=DataContext.IsReadOnly}" >
<TextBox.Text>
<Binding Path="Markup" StringFormat="N2" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBlock Name="Promotion" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=PromoFriendlyDescription}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5"
Visibility="{Binding ElementName=UserControlPriceList, Path=DataContext.HideIfEditing}"/>
<ComboBox Name="PromosComboBox" AutomationProperties.Name="PromoDiscount" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left"
Text="(Seleccione promo)"
ItemsSource="{Binding ElementName=UserControlPriceList, Path=DataContext.PromoCollection}"
SelectedValue="{Binding ElementName=UserControlPriceList, Path=DataContext.Current.PromoId, Mode=TwoWay}"
SelectedValuePath="Id"
Style="{StaticResource ComboBoxStyle}"
ItemContainerStyle="{StaticResource ComboBoxItemStyle}" Margin="8,5,0,5"
IsTextSearchEnabled="True"
IsTextSearchCaseSensitive="False"
IsDropDownOpen="False"
StaysOpenOnEdit="True"
IsEditable="True" IsReadOnly="False"
Visibility="{Binding ElementName=UserControlPriceList, Path=DataContext.ShowIfEditing}"
Width="{Binding ElementName=Promotion, Path=Width}">
<ComboBox.Resources>
<DataTemplate DataType="{x:Type localModel:Promo}">
<TextBlock Text="{Binding Converter={StaticResource promoConverter}, ConverterParameter=PromoFriendlyDescription}"/>
</DataTemplate>
</ComboBox.Resources>
</ComboBox>
<TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
</Grid>
</Border>
</DataTemplate>
</UserControl.Resources>
<Border Padding="20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource MediumTitleStyle}" Margin="5"
Text="{Binding Path=List.Name, StringFormat='Lista: {0}'}" />
<TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Style="{StaticResource MediumTitleStyle}" Margin="5" HorizontalAlignment="Right"
Text="{Binding Path=List.PrintDate, StringFormat='Última impresión: {0:dd/MM/yyyy}'}" />
<Border Grid.Row="1" Style="{StaticResource BorderStyle}" Grid.ColumnSpan="4">
<ListView Name="ListViewPriceListItems" Grid.Row="1" Grid.ColumnSpan="4"
ItemsSource="{Binding Path=Items}"
IsSynchronizedWithCurrentItem="True"
SelectionChanged="OnPriceListItemSelectionChanged"
MouseDoubleClick="EnterPriceListItemEditMode"
IsEnabled="{Binding Path=IsReadOnly}">
<ListView.View>
<GridView AllowsColumnReorder="true" ColumnHeaderToolTip="Detalle de productos, precios finales y promociones">
<GridViewColumn DisplayMemberBinding="{Binding Path=Product.Description}" Header="Producto" Width="300"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Product.CostPrice, StringFormat=C}" Header="Costo"/>
<GridViewColumn DisplayMemberBinding="{Binding Path=Markup, StringFormat=N2}" Header="Markup (%)" />
<GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}" Header="Precio Lista"/>
<GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=PromoFriendlyDescription}" Header="Promoción" Width="200" />
<GridViewColumn DisplayMemberBinding="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}" Header="Precio Final" />
</GridView>
</ListView.View>
</ListView>
</Border>
<ContentControl Name="PriceListDetail" Grid.Row="2" Grid.ColumnSpan="4"
Content="{Binding Path=Current}"
ContentTemplate="{StaticResource PriceListItemDetailTemplate}"
Margin="9,0,0,0" />
<StackPanel Grid.Row="3" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Right">
<StackPanel.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Margin" Value="0,0,0,0"/>
</Style>
</StackPanel.Resources>
<Button Name="EditButton" HorizontalAlignment="Right" Content="_Modificar" Style="{StaticResource AcctionButtonStyle}"
ToolTip="Editar precio y promoción del producto seleccionado en esta lista."
Click="EditSelectedPriceListItem" Visibility="{Binding Path=HideIfEditing}"/>
<Button Name="SaveButton" HorizontalAlignment="Right" Content="_Aceptar" Style="{StaticResource ActionButtonOKStyle}"
ToolTip="Guardar los cambios realizados."
Click="SavePriceListItem" Visibility="{Binding Path=ShowIfEditing}"/>
<Button Name="CancelButton" HorizontalAlignment="Right" Content="_Cancelar" Style="{StaticResource ActionButtonCancelStyle}"
ToolTip="Deshacer los cambios realizados."
Click="UndoPriceListItem" Visibility="{Binding Path=ShowIfEditing}"/>
</StackPanel>
</Grid>
</Border>
This is its view model:
public partial class PriceListItemsViewModel: BaseViewModel<PriceListItem>
{
private PriceList _list;
public PriceListItemsViewModel(PriceList list, ObservableCollection<Promo> promoCollection)
{
this.List = list ?? throw new ArgumentNullException(nameof(list));
this.PromoCollection = promoCollection;
this.Items = list.Products;
this.IsEditMode = false;
this.IsNew = false;
this.Current = this.Items.FirstOrDefault();
}
public PriceList List { get => this._list; set => this._list = value; }
public ObservableCollection<Promo> PromoCollection { get; }
}
Note: BaseViewModel has an ObservableCollection, implements Generic INotifyPropertyChange, and common properties to indicate the current selected item in that collection and other useful states
And here the wpf of the view containing the previous user control:
<Window.Resources>
<uc:PriceListUserControl x:Key="userControlPList"/>
</Window.Resources>
<Border Padding="20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TabControl x:Name="TabControlPriceLists" Grid.Row="1" Grid.ColumnSpan="3" TabStripPlacement="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding Current}"
IsEnabled="{Binding IsReadOnly}" >
<TabControl.Resources>
<DataTemplate DataType="{x:Type localVm:PriceListItemsViewModel}">
<uc:PriceListUserControl x:Name="UserControlPriceList"/>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding List.Name}" />
<Setter Property="MaxWidth" Value="100" />
<Setter Property="ToolTip" Value="{Binding List.Name}" />
<Setter Property="Background" Value="Bisque" />
<Setter Property="FontWeight" Value="DemiBold" />
<EventSetter Event="MouseDoubleClick" Handler="OnTabItemDoubleClick"/>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
</Grid>
</Border>
And its view model:
public class PriceListsViewModel : BaseViewModel<PriceListItemsViewModel>
{
private ObservableCollection<Promo> promoCollection;
public PriceListsViewModel()
{
var promoDefaultItems = new List<Promo>
{
new Promo { FriendlyName = PromoConverter.PromoFriendlyNameNoPromo, DiscountPct = 0.0F } //Workaround: Adds "(No promo) to allow the user to left an item without promotion
//new Promo { FriendlyName = PromoConverter.PromoFriendlyNameNewPromo, DiscountPct = 0F }
};
this.promoCollection = new ObservableCollection<Promo>(promoDefaultItems);
var promos = PromoRepository.PromoGetAll();
foreach (var promo in promos)
{
this.promoCollection.Add(promo);
}
var lists = PriceListRepository.PriceListGetAll();
this.Items = new ObservableCollection<PriceListItemsViewModel>();
foreach (var l in lists)
{
this.Items.Add(new PriceListItemsViewModel(l, promoCollection));
}
this.IsEditMode = false;
this.IsNew = false;
this.Current = this.Items.FirstOrDefault();
}
}
I resolved the problem by creating a new view model wrapping the former PriceListItem and providing real properties and notifying their changes.
This is the new view model (only relevant code to show the new properties)
public class PriceListItemViewModel : SupervisedEntity
{
private PriceListItem _item;
public PriceListItemViewModel(PriceListItem item)
{
this._item = item ?? throw new ArgumentNullException(nameof(item));
//Bind the parent properties to its dependants (actually they are computed properties without setter) so they notify property changes when their parent property changes.
this.AddDependentProperty(nameof(this.PromoId), nameof(this.SalePrice));
this.AddDependentProperty(nameof(this.PromoId), nameof(this.FinalPrice));
this.AddDependentProperty(nameof(this.PromoId), nameof(this.PromoFriendlyDescription));
this.AddDependentProperty(nameof(this.Markup), nameof(this.SalePrice));
this.AddDependentProperty(nameof(this.Markup), nameof(this.FinalPrice));
}
// The properties and code not related to the solution were removed for clarity
public int? PromoId
{
get => this._item.PromoId;
set
{
this._item.PromoId = (value is null || value == 0) ? null : value;
this.NotifyPropertyChanged(); //This notifies not only the change in PromoId but also in all its dependent properties
}
}
public string ProductDescription => this._item.Product.Description;
public float CostPrice => this._item.Product.CostPrice;
[Required]
public float Markup
{
get => this._item.Markup;
set
{
this._item.Markup = value;
this.NotifyPropertyChanged();
}
}
//Using properties instead of extension methods allows me to create notification for them as dependent
public float SalePrice => this._item.SalePrice();
public float FinalPrice => this._item.FinalPrice();
public int Id => this.Item.Id;
public string PromoFriendlyDescription => this._item.PromoId is null ? string.Empty : this._item.Promotion.ToString();
}
And in the xaml of the user control I replaced all the converters, which didn't get notified when one of the properties they depended on changed, by the actual properties in the new view model.
First, replacing the class of the model by the new view model, like this:
<DataTemplate x:Key="PriceListItemDetailTemplate" DataType="{x:Type localVm:PriceListItemViewModel}">
And then replacing all the references to converters in the properties that change:
<TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=SalePrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Converter={StaticResource priceListItemConverter}, ConverterParameter=FinalPrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
By the real properties in the new view model:
<TextBlock Name="SalePrice" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Path=SalePrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
<TextBlock Name="FinalPrice" Grid.Row="5" Grid.Column="1" HorizontalAlignment="Left"
Text="{Binding Path=FinalPrice, StringFormat=C}"
Style="{StaticResource TextStyleTextBlockTextBoxLike}" Margin="8,5,0,5" />
And now it works.

Textbox two way binding not triggering

I have a tab control with 3 objects, 2 lists and a textbox. The text box is bound two way :
<TabControl x:Name="tcTabs" ItemsSource="{Binding Rooms, UpdateSourceTrigger=PropertyChanged}" Margin="5" BorderThickness="1" IsSynchronizedWithCurrentItem="True">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Header" Value="{Binding Name}" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="22"/>
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding ReceivedMessages}" DisplayMemberPath="Raw" Grid.Row="0" Grid.Column="0" BorderThickness="0" />
<ListBox ItemsSource="{Binding Users}" DisplayMemberPath="Nick" Visibility="{Binding Type, Converter={StaticResource UserListVisibilityConverter}}" Grid.Row="0" Grid.Column="1" BorderThickness="1,0,0,0" BorderBrush="#FFBBBBBB" Width="130" />
<TextBox Text="{Binding CurrentInput, Mode="TwoWay"}" Grid.Row="1" Grid.ColumnSpan="2" BorderThickness="0,1,0,0" BorderBrush="#FFBBBBBB" Height="22" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
</TabControl>
Backing object :
public string CurrentInput
{
get
{
return _currentInput;
}
set
{
if (value != _currentInput)
{
_currentInput = value;
OnPropertyChanged();
}
}
}
Problem is, when I change the text and click another tab it does not update the backing field (does not even hit the setter), however if I change then click the listbox it does...
Any reason for this odd behaviour?
That is not an odd behaviour and has been asked multiple times before. Read about Binding.UpdateSourceTrigger, also see the remarks of the respective property you bind.
I've solve this problem (Twoway Binding) by manual trigger the databinding engine using
DataContext = this;

How to setup a grid as template for an Items control?

I'm trying to create an ItemsControl that uses a grid as its ItemsPanel in such a way that it has two columns, where the first columns width is the width of the widest item in that column, and has as may rows needed to display all the items
Basically, I want the following, but somehow within an ItemsControl so that I can bind to a collection of objects:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Label Content="{Binding Items[0].Header}"/>
<TextBox Text="{Binding Items[0].Content}" Grid.Column="1"/>
<Label Content="{Binding Items[1].Header}" Grid.Row="1"/>
<TextBox Text="{Binding Items[1].Content}" Grid.Row="1" Grid.Column="1"/>
<Label Content="{Binding Items[2].Header}" Grid.Row="2"/>
<TextBox Text="{Binding Items[2].Content}" Grid.Row="2" Grid.Column="1"/>
</Grid>
Edit : Rachels answer worked great, here is a working example.
(I moved the Grid.IsSharedSizeScope="True" to the ItemsPanel, not sure if Rachel meant to put it in the ItemTemplate (which didn't work))
namespace WpfApplication23
{
public partial class Window1 : Window
{
public List<Item> Items { get; set; }
public Window1()
{
Items = new List<Item>()
{
new Item(){ Header="Item0", Content="someVal" },
new Item(){ Header="Item1", Content="someVal" },
new Item(){ Header="Item267676", Content="someVal" },
new Item(){ Header="a", Content="someVal" },
new Item(){ Header="bbbbbbbbbbbbbbbbbbbbbbbbbb", Content="someVal" },
new Item(){ Header="ccccccc", Content="someVal" }
};
InitializeComponent();
DataContext = this;
}
}
public class Item
{
public string Header { get; set; }
public string Content { get; set; }
}
}
<Window x:Class="WpfApplication23.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Grid.IsSharedSizeScope="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="ColumnOne" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{Binding Header}"/>
<TextBox Text="{Binding Content}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
There are multiple problems here for an ItemsControl:
Getting your first column to match the width of the largest item
Generating a dynamic number of rows
Generating more than one item for each iteration of the ItemsControl
The last one is really the biggest problem, because an ItemsControl wraps each ItemTemplate in a ContentPresenter, so there is no default way of creating more than one item in the panel for each Iteration of the ItemsControl. Your end result would look like this:
<Grid>
...
<ContentPresenter>
<Label Content="{Binding Items[0].Header}"/>
<TextBox Text="{Binding Items[0].Content}" Grid.Column="1"/>
</ContentPresenter>
<ContentPresenter>
<Label Content="{Binding Items[1].Header}" Grid.Row="1"/>
<TextBox Text="{Binding Items[1].Content}" Grid.Row="1" Grid.Column="1"/>
</ContentPresenter>
<ContentPresenter>
<Label Content="{Binding Items[2].Header}" Grid.Row="2"/>
<TextBox Text="{Binding Items[2].Content}" Grid.Row="2" Grid.Column="1"/>
</ContentPresenter>
</Grid>
My best suggestion would be to create an ItemTemplate that contains a 1x2 Grid, and use Grid.IsSharedSizeScope to make the width of the first column shared. (The ItemsPanelTemplate would remain the default StackPanel.)
This way, the end result would look like this:
<StackPanel>
<ContentPresenter>
<Grid IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="ColumnOne" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{Binding Header}"/>
<TextBox Text="{Binding Content}" Grid.Column="1"/>
</Grid>
</ContentPresenter>
<ContentPresenter>
<Grid IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="ColumnOne" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Content="{Binding Header}"/>
<TextBox Text="{Binding Content}" Grid.Column="1"/>
</Grid>
</ContentPresenter>
...
</StackPanel>
You can use a ListView
<ListView ItemsSource="{Binding MyList}">
<ListView.View>
<GridView>
<GridView.ColumnHeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</GridView.ColumnHeaderContainerStyle>
<GridViewColumn
Header=""
Width="Auto"
DisplayMemberBinding="{Binding Header}"/>
<GridViewColumn
Header=""
DisplayMemberBinding="{Binding Value}"/>
</GridView>
</ListView.View>
</ListView>
the ColumnHeaderContainerStyle hides the GridViewHeader

Sharing control instance within a view in WPF

I'm having some issues with the wpf tab control. Not sure the title of the question is right I will refine it accoring to answers.
I want to create a simple panel system. I want to inject ot my "panel viewModel" 2 view model
MainViewModel will be display as the main area
PanelViewModel will be display as a panel on the right hand side of the view
the panelViewModel will be hidden by default and a button will display it on top of the main view model when needed
The view look like this:
<UserControl.Resources>
<DataTemplate x:Key="MainWindowTemplate" DataType="{x:Type UserControl}">
<ContentPresenter Content="{Binding DataContext.MainViewModel, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" />
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid Visibility="{Binding IsPanelHidden, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" ContentTemplate="{StaticResource MainWindowTemplate}" />
<Button Grid.Column="1" Content="{Binding PanelTitle}" Command="{Binding Path=ShowPanelCommand}">
<Button.LayoutTransform>
<RotateTransform Angle="90"/>
</Button.LayoutTransform>
</Button>
</Grid>
<Grid Visibility="{Binding IsPanelHidden, Converter={StaticResource revertBool2VisibilityConverter}}">
<ContentControl ContentTemplate="{StaticResource MainWindowTemplate}" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="0.5*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="2" VerticalAlignment="Stretch" Background="Red" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border HorizontalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding PanelTitle}" Margin="5,5,0,2" HorizontalAlignment="Left"></TextBlock>
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="0,0,5,0" HorizontalAlignment="Right" >
<Button Content="Minimze" Command="{Binding HidePanelCommand}"/>
</StackPanel>
</Grid>
</Border>
<ContentPresenter Grid.Row="1" Margin="2" Content="{Binding PanelViewModel}" VerticalAlignment="Top"/>
</Grid>
</Border>
</Grid>
</Grid>
The view model look like that:
public class TestTabViewModel : ObservableObject
{
#region private attributes
#endregion
public TestTabViewModel(string panelName, object panelViewModel, object mainViewModel)
{
IsPanelHidden = true;
PanelTitle = panelName;
PanelViewModel = panelViewModel;
MainViewModel = mainViewModel;
ShowPanelCommand = new DelegateCommand(() =>ManagePanelVisibility(true));
HidePanelCommand = new DelegateCommand(() => ManagePanelVisibility(false));
}
#region properties
public string PanelTitle { get; private set; }
public bool IsPanelHidden { get; private set; }
public object PanelViewModel { get; private set; }
public object MainViewModel { get; private set; }
public DelegateCommand ShowPanelCommand { get; private set; }
public DelegateCommand HidePanelCommand { get; private set; }
#endregion
#region private methods
private void ManagePanelVisibility(bool visible)
{
IsPanelHidden = !visible;
RaisePropertyChanged(() => IsPanelHidden);
}
#endregion
}
So for so good, this system work fine I aslo added some pin command but I remove them from here to make it "simple".
My problem come when the main view model hold a tab control. In this, case if I select a tab and "open" the panel, the tab selected is "changed". In fact it's not changed it's just that I display another contentControl which is not synchronize with the previouse one. I guess that the view instance is not the same even if the viewmodel behind is.
So how do I share a view instance within a view (or have the selection process synchornized)? My first guest was to use the datatemplate (as show in the example) but it did not solve my problem.
By the way, I know some third-party handling panel docking pin ... (eg avalon) but all the one I found are really too much for my simple need.
Thanks for the help
Your best bet would probably be to replace your two Grids with a single ContentControl, and switch the Template on button click or in a Trigger. This way your actual Content (the TabControl) will be the same, but the template used to display the Content will change
Here's a quick example:
<Window.Resources>
<ControlTemplate x:Key="Grid1Template" TargetType="{x:Type ContentControl}">
<DockPanel>
<Grid Background="CornflowerBlue" Width="100" DockPanel.Dock="Left" />
<ContentPresenter Content="{TemplateBinding Content}" />
</DockPanel>
</ControlTemplate>
<ControlTemplate x:Key="Grid2Template" TargetType="{x:Type ContentControl}">
<DockPanel>
<Grid Background="CornflowerBlue" Width="100" DockPanel.Dock="Right" />
<ContentPresenter Content="{TemplateBinding Content}" />
</DockPanel>
</ControlTemplate>
</Window.Resources>
<DockPanel>
<ToggleButton x:Name="btnToggle" Content="Toggle View" DockPanel.Dock="Top" />
<ContentControl>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Template" Value="{StaticResource Grid1Template}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=btnToggle, Path=IsChecked}" Value="True">
<Setter Property="Template" Value="{StaticResource Grid2Template}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
<TabControl>
<TabItem Header="Tab1" />
<TabItem Header="Tab2" />
<TabItem Header="Tab3" />
</TabControl>
</ContentControl>
</DockPanel>
i dont know if i get what you want but i think that you could do the following.
i assume that you want some "MainViewmodeldata" be presented as your tabcontrol.
so i woulf first create a datatemplate for this.
<UserControl.Resources>
<DataTemplate DataType="{x:Type MainViewmodeldata}">
<TabControl>
<TabItem Header="Tab1">
<TextBlock Grid.Column="1" Text="Tab1Content"/>
</TabItem>
<TabItem Header="Tab2">
<TextBlock Grid.Column="1" Text="Tab2Content"/>
</TabItem>
</TabControl>
</DataTemplate>
</UserControl.Resources>
now i would just bind my this mainviewmodeldata to the contentcontrol and let wpf render it for you. i really dont know if you still need these two grids, cause i dont know what you wanna achieve.
<Grid x:Name="Grid1" Visibility="{Binding IsPanelHidden, Converter={StaticResource bool2VisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentControl Content="{Binding MainViewmodelData}" />
<StackPanel Grid.Column="1">
<Button x:Name="Button1" Content="Switch look" Command="{Binding ShowPanelCommand}"/>
<TextBlock Text="Look1"/>
</StackPanel>
</Grid>
<Grid x:Name="Grid2" Visibility="{Binding IsPanelHidden, Converter={StaticResource revertBool2VisibilityConverter}}">
<ContentControl Content="{Binding MainViewmodelData}" />
<StackPanel HorizontalAlignment="Right">
<Button x:Name="Button2" Content="Switch look" Command="{Binding HidePanelCommand}"/>
<TextBlock Text="Look2"/>
</StackPanel>
</Grid>
</Grid>

Changing ContentTemplate based on ListBox selection

I have a Listbox and a Border in a StackPanel similar to the following:
<StackPanel Orientation="Horizontal">
<ListBox>
<ListBoxItem Content="People"/>
<ListBoxItem Content="Animals"/>
<ListBoxItem Content="Cars"/>
</ListBox>
<Border Width="200>
<ContentPresenter/>
</Border>
</StackPanel>
When selecting an item in the listbox I would like to change the content in the ContentPresenter accordingly e.g. selecting People would change the template to display a series of input fields related to people where as selecting Animals would display a series of fields related to Animals etc. - the behavior of this would be akin to a TabControl.
I think I can achieve this with a DataTrigger which changes the DataTemplate in the Border but I'm not sure how to achieve this.
Any pointers?
Thanks
You can toggle the ContentTemplate using a DataTrigger as follows.
Note, that I am binding an ObservableCollection to a simple object (Thing) with one property called Name, and am I binding the Content of the ContentControl to the SelectedItem in the ListBox using a ViewModel.
<Grid>
<Grid.Resources>
<local:MultiValueConverter x:Key="con" />
<DataTemplate x:Key="PeopleTemplate">
<StackPanel Orientation="Horizontal">
<Label Margin="0,0,5,0" Content="People Name" HorizontalAlignment="Left" Grid.Column="0" />
<TextBox Grid.Column="1" Width="100" Height="25"></TextBox>
<Button Content="OK" Grid.Column="2" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="AnimalsTemplate">
<StackPanel Orientation="Horizontal">
<Label Margin="0,0,5,0" Content="Animal Name" HorizontalAlignment="Left" Grid.Column="0" />
<TextBox Grid.Column="1" Width="100" Height="25"></TextBox>
<Button Content="OK" Grid.Column="2" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="CarsTemplate">
<StackPanel Orientation="Horizontal">
<Label Margin="0,0,5,0" Content="Car Name" HorizontalAlignment="Left" Grid.Column="0" />
<TextBox Grid.Column="1" Width="100" Height="25"></TextBox>
<Button Content="OK" Grid.Column="2" />
</StackPanel>
</DataTemplate>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<ListBox ItemsSource="{Binding Things}" SelectedItem="{Binding SelectedThing}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0" Orientation="Horizontal">
<TextBlock Padding="5" Text="{Binding Name}" Margin="0"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border Width="200">
<ContentControl Content="{Binding SelectedThing}">
<ContentControl.ContentTemplate>
<DataTemplate>
<ContentControl Name="cc"
Content="{Binding}"
ContentTemplate="{StaticResource PeopleTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Name}" Value="People">
<Setter TargetName="cc"
Property="ContentTemplate"
Value="{StaticResource PeopleTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Name}" Value="Animals">
<Setter TargetName="cc"
Property="ContentTemplate"
Value="{StaticResource AnimalsTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Name}" Value="Cars">
<Setter TargetName="cc"
Property="ContentTemplate"
Value="{StaticResource CarsTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Border>
</StackPanel>
<Grid>
Here is the Thing class:
public class Thing
{
public Thing(String name)
{
this.Name = name;
}
public String Name { get; set; }
public static ObservableCollection<Thing> GetThingList()
{
return new ObservableCollection<Thing>(new Thing[3] {
new Thing("People"),
new Thing("Animals"),
new Thing("Cars")
});
}
}

Resources