Validating items in ItemsControl - c#

I am developing a WPF application and in one window I used a wizard component from WPF toolkit. In this wizard I'm creating a new person. In second step I am using an enumeration as a source for possible contact types (for example Phone, Email...).
This is my wizard page in XAML:
<xctk:WizardPage x:Name="NewContactPage" PageType="Interior"
Title="Contacts" Style="{DynamicResource NewContactPage}"
CanCancel="True" CanFinish="False"
Loaded="NewContactPage_Loaded"
PreviousPage="{Binding ElementName=NewPersonPage}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top">
<control:DataLoader x:Name="ctrNewContactLoader" />
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top" Orientation="Vertical">
<ItemsControl ItemsSource="{Binding Path=Person.PersonContacts, Mode=TwoWay,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Window}}"
Name="icContacts">
<ItemsControl.ItemTemplate>
<ItemContainerTemplate>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top" Orientation="Vertical"
Margin="5" Background="WhiteSmoke">
<CheckBox IsChecked="{Binding Path=IsValid}"
Content="{Binding Path=ContactType.Description}"
Name="cbContactVisible"/>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Top"
Visibility="{Binding ElementName=cbContactVisible, Path=IsChecked,
Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Stretch" MaxLength="64"
Name="txtContactValue"
Text="{Binding Path=Contact,
ValidatesOnDataErrors=True,
ValidatesOnNotifyDataErrors=True,
ValidatesOnExceptions=True}" />
</Grid>
</StackPanel>
</ItemContainerTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
The source of ItemsControl is a List of PersonContactModel class:
public class PersonContactModel : BaseObjectModel
{
public PersonContactModel()
{
this.Created = DateTime.Now;
this.Updated = DateTime.Now;
this.IsValid = true;
this.ContactType = new ContactTypeModel();
}
public string Contact { get; set; }
public ContactTypeModel ContactType { get; set; }
public DateTime Created { get; set; }
public int Id { get; set; }
public bool IsValid { get; set; }
public DateTime Updated { get; set; }
public override string this[string columnName]
{
get
{
string retVal = string.Empty;
switch (columnName)
{
case "Contact":
retVal = base.Concat(base.RequeiredField(this.Contact), base.MinLength(this.Contact, 5), base.MaxLength(this.Contact, 62));
break;
}
return retVal;
}
}
}
the base class implement a IDataErrorInfo interface with validation info about Contact property.
The desired behavior is that if the checkbox is checked, it is visible grid with a field for entering a contact, otherwise not. Button next step should be seen only when selected contact types are valid. This functionality is trying to accomplish the following styles in app.xaml:
<Style TargetType="xctk:WizardPage" x:Key="NewContactPage">
<Setter Property="NextButtonVisibility" Value="Hidden" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=(Validation.HasError), ElementName=txtContactValue}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="NextButtonVisibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
Unfortunately, the button for the next step is invisible, even if it asks all kinds of contact for the new person and will fulfill all the conditions for a valid entry.
What's wrong? Where is an error?

You are trying to achieve what you want in a not very good way. Error in this particular code is because you reference element "txtContactValue" from your style trigger, and style has no idea at all what this element is. By the way, if you look at output window when debugging your code, I bet you will see this error there.
Now, even if you will try to reference "txtContactValue" without style, like this:
NextButtonVisibility="{Binding ElementName=txtContactValue, Path=(Validation.HasError), Converter={StaticResource BooleanToVisibilitConverter}}"
It won't work, because txtContactValue is in different scope. BUT you should not do this in a first place! You have a model for your data, and that is model which controls if data is valid or not. Just add some property to your model which indicates if data which you create on this wizard page is valid (like PersonContact.IsValid) and you can proceed to the next page, and bind to this property.

Related

Binding data from datagrid to ui components

I have a window with a datagrid getting his items from an ObservableCollection and I have some field on the side which I want to set from the selected row on the datagrid.
Problem is when I select a row the fields they are not set; how do they get set?
View
<Window x:Class="videotheque.View.FilmView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:videotheque.View"
xmlns:vm="clr-namespace:videotheque.ViewModel"
mc:Ignorable="d"
Title="Vos films" Height="480" Width="900">
<Window.DataContext>
<vm:FilmViewModel/>
</Window.DataContext>
<Grid>
<DataGrid SelectedItem="{Binding Path=SelectedMovie, Mode=TwoWay}" ItemsSource="{Binding ListFilm, Mode=TwoWay}" CanUserAddRows="False" AutoGenerateColumns="False" Width="499" HorizontalAlignment="Left" Margin="10,0,0,68" Height="371" VerticalAlignment="Bottom">
<DataGrid.Columns>
<DataGridTextColumn Header="Titre" Binding="{Binding Titre}" Width="150"/>
<DataGridTextColumn Header="Durée" Binding="{Binding Duree}"/>
<DataGridTextColumn Header="Age Min." Binding="{Binding AgeMinimum}"/>
<DataGridTextColumn Header="Langue" Binding="{Binding LangueMedia}" Width="50"/>
<DataGridTextColumn Header="Sous titres" Binding="{Binding SousTitres}" Width="60"/>
<DataGridTemplateColumn Header="Modifier">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button CommandParameter="{Binding Id}"
Command="{Binding Path=DataContext.ModifierLeMediaCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
Modifier
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Supprimer">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button CommandParameter="{Binding Id}"
Command="{Binding Path=DataContext.SupprimerLeMediaCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
Supprimer
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Label Content="Titre :" HorizontalAlignment="Left" Margin="520,11,0,0" VerticalAlignment="Top" Width="39"/>
<TextBlock Text="{Binding SelectedMovie.Titre, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="568,16,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="0.362,-1.709"/>
<CheckBox IsChecked="{Binding SelectedMovie.Vu, Mode=TwoWay}" Content="Vu" HorizontalAlignment="Left" Margin="709,17,0,0" VerticalAlignment="Top"/>
<Label Content="Note :" HorizontalAlignment="Left" Margin="521,42,0,0" VerticalAlignment="Top"/>
<TextBlock HorizontalAlignment="Left" Margin="568,47,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top"/>
<Label Content="Synopsis :" HorizontalAlignment="Left" Margin="521,82,0,0" VerticalAlignment="Top"/>
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="525,113,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="95" Width="209"/>
<Label Content="Commentaire :" HorizontalAlignment="Left" Margin="519,223,0,0" VerticalAlignment="Top"/>
<TextBlock HorizontalAlignment="Left" Margin="525,254,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="128" Width="218"/>
<CheckBox IsChecked="{Binding SelectedMovie.SupportPhysique, Mode=TwoWay}" Content="Support physique" HorizontalAlignment="Left" Margin="709,56,0,0" VerticalAlignment="Top"/>
<CheckBox IsChecked="{Binding SelectedMovie.SupportNumerique, Mode=TwoWay}" Content="Support numérique" HorizontalAlignment="Left" Margin="709,88,0,0" VerticalAlignment="Top"/>
<Button Command="{Binding Path=DataContext.AjouterFilmCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
Content="Ajouter un film" HorizontalAlignment="Left" Margin="10,405,0,0" VerticalAlignment="Top" Width="182"/>
</Grid>
ViewModel (did not past all code, there is juste some command function for the buttons which works):
class FilmViewModel
{
public ObservableCollection<Media> ListFilm { get; set; }
private ICommand _modifierLeMediaCommand;
private ICommand _supprimerLeMedia;
private Media SelectedMovie;
public FilmViewModel()
{
this.ListFilm = new ObservableCollection<Media>();
this.LoadData();
}
public async void LoadData()
{
var context = await DataAccess.VideothequeDbContext.GetCurrent();
foreach (Media f in context.Medias.Where(m => m.TypeMedia.ToString() == "Film").ToList())
{
this.ListFilm.Add(f);
}
}
}
And the model :
public class Media
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime DateSortie { get; set; }
public TimeSpan Duree { get; set; }
public string Titre { get; set; }
public bool Vu { get; set; }
public int Note { get; set; }
public string Commentaire { get; set; }
public string Synopsis { get; set; }
public ETypeMedia.TypeMedia TypeMedia { get; set; }
public int AgeMinimum { get; set; }
public bool SupportPhysique { get; set; }
public bool SupportNumerique { get; set; }
public string Image { get; set; }
public ELangue.Langue LangueVO { get; set; }
public ELangue.Langue LangueMedia { get; set; }
public ELangue.Langue SousTitres { get; set; }
[InverseProperty(nameof(GenreMedia.Media))]
public List<GenreMedia> Genres { get; set; }
[InverseProperty(nameof(PersonneMedia.Media))]
public List<PersonneMedia> Personnes { get; set; }
[InverseProperty(nameof(Episode.Media))]
public List<Episode> Episodes { get; set; }
}
I did hear about INotifyPropertyChanged, but it looks like so long to do with what I have so I dont know...
Take your control which has a binding that says ="{Binding SelectedMovie.Vu, ...}".
Remember that Binding is just reflection into the instance object of what is defined which is to go to SelectedMovie.Vu.
The SelectedMovie property is set to private and reflection into that fails right there (issue #1).
So let us make SelectedMovie public. It still fails! Even when the selection changes the binding does not get the current data. Why?
The why is that when the control is placed on the screen it attempts a binding at that time which the property is Null. So it does not show anything.
When the property changes, the binding is still failing because it is not receiving an event for a change so it can replace the null value.
Why?
The property does not follow INotifyPropertyChanged so no event announces that a change has been made because the control's binding will listen to a change event for SelectedMovie, but SelectedMovie never announces a change.
But there is a possible third problem even after the SelectedMovie reports a change by using INotifyPropertyChanged. Because the binding is looking for a child actual change notification from .Vu.
The .Vu does not announce a change because it also doesn't do InotifyPropertyChanged. Even if it did...it doesn't announce a change when SelectedMovie is set. How would it know?
You can make your binding system work, but you are going to have to do some extra notify events when the top level object is set. What you should use is a binding to the named DataGrid itself. Here is an example using a similar ListBox:
Example
In our Window or Page or Control I am going to setup some data in the XAML for our example.
Data
<Window.Resources>
<model:Orders x:Key="Orders">
<model:Order CustomerName="Alpha"
OrderId="997"
InProgress="True" />
<model:Order CustomerName="Beta"
OrderId="998"
InProgress="False" />
<model:Order CustomerName="Omega"
OrderId="999"
InProgress="True" />
<model:Order CustomerName="Zeta"
OrderId="1000"
InProgress="False" />
</model:Orders>
</Window.Resources>
ListBox
Then I am going to host/bind the data in a ListBox in which every line will be a TextBox to show the CustomerName.
<ListBox ItemsSource="{StaticResource Orders}" x:Name="lbOrders">
<ListBox.Resources>
<DataTemplate DataType="{x:Type model:Order}">
<TextBlock Text="{Binding Path=CustomerName}" />
</DataTemplate>
<Style TargetType="{x:Type ListBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=InProgress}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.Resources>
</ListBox>
So we have a listbox that shows the data:
Show Selected Item's Order Number
So now we add another control under it which will show the user's selected item's order number. We will bind by using the ElementName which will point to our listbox control's SelectedItem dependancy property with the name of the control we have given which is lbOrders. On that property it will hold the selected instance and we will drill down to OrderId.
<Label Content="{Binding SelectedItem.OrderId, ElementName=lbOrders}"/>
So when we select Omega we get "999" shown.

Combobox : DataTemplate for each DataTrigger

I have two problems, one small and one big, and I need help;
First, this is my code :
<ComboBox Name="cmb1" Width="165" Height="25" Margin="25,5,10,10"
ItemsSource="{Binding ChoicesList}"
SelectedItem="{Binding SelectedChoiceList}">
</ComboBox>
<ComboBox Name="cmb2" Width="165" Height="25" Margin="10,5,25,10">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="ItemsSource" Value="{Binding Sections}"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedChoiceList}" Value="Contract">
<Setter Property="ItemsSource" Value="{Binding Contract}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedChoiceList}" Value="Service">
<Setter Property="ItemsSource" Value="{Binding Services}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding Name}"
Margin="4,0"/>
<TextBlock Grid.Column="0"
Text="{Binding label}"
Margin="0"/>
<TextBlock Grid.Column="0"
Text="{Binding Start, StringFormat=d}"
Margin="0"/>
<TextBlock Grid.Column="1"
Text="{Binding End,StringFormat=d}"
Margin="0"/>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Another day, another problem!
In cmb2, I would like nothing to be loaded until I have made one of the three choices, is there a way?
(for the moment I load "section" list if nothing is chosen, and the binding changes according to the choices. I made this choice because the value of "section" can change, it can be "A" or "B" or "C" and more, so I would like "service" = service list, "contract" = contract list and "section"= all other values)
More importantly, I would like to rework this code because currently the DataTemplate is the same for all three binds, which was not awkward (If I choose "section", textblock displays "name" selected, cause section have no label, for example) but I would like, for contracts, to display "name" + "from" + "Starting date" + "To" + "Ending date".
If I add textblock "text = from" and "text = to", it will apply to all my choices, and I would have, for example, if I choose "section", a result like "nameOfSection + from + to", and I don't want that.
So I would like the textblock "from" and "to" to appear only for the choice "contract", I don't know if it's possible?
Hoping to have been clear, and thanking you for your time for the help you will give me;
Welcome to SO!
To answer your first point by removing the line that is setting the ItemSource you should be able to have nothing loaded
<Setter Property="ItemsSource" Value="{Binding Sections}"></Setter> can be removed and replaced with another DataTrigger.
To answer your second point you can use DataTemplates with the DataType property to apply a style to a specific data type. This means you can setup each of your, Service, Section and Contract to look differently / template their own properties.
<DataTemplate DataType="{x:Type classes:Contract}">
<Label Content="Contract"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Service}">
<Label Content="Service"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Section}">
<Label Content="Section"/>
</DataTemplate>
This bit of code demonstrates that.
Here is also a sample application I have made that should do what you've asked.
View
<Window x:Class="SOHelp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:classes="clr-namespace:SOHelp.Classes"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type classes:Contract}">
<Label Content="Contract"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Service}">
<Label Content="Service"/>
</DataTemplate>
<DataTemplate DataType="{x:Type classes:Section}">
<Label Content="Section"/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox Grid.Row="0" Width="165" Height="25"
ItemsSource="{Binding ChoicesList}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedChoiceList}"/>
<ComboBox Grid.Row="1" Width="165" Height="25"
ItemsSource="{Binding SelectedChoiceList.Items}"/>
</Grid>
</Window>
Code behind / could be ViewModel
public partial class MainWindow : Window, INotifyPropertyChanged
{
private ObservableCollection<Option> _choicesList;
private Option _selectedChoiceList;
public Option SelectedChoiceList
{
get { return _selectedChoiceList; }
set { _selectedChoiceList = value; NotifyPropertyChanged(); }
}
public ObservableCollection<Option> ChoicesList
{
get { return _choicesList; }
set { _choicesList = value; NotifyPropertyChanged(); }
}
public MainWindow()
{
InitializeComponent();
ChoicesList = new ObservableCollection<Option>
{
new Option("Contract", new ObservableCollection<object>
{
new Contract(), new Contract(), new Contract()
}),
new Option("Service", new ObservableCollection<object>
{
new Service(), new Service(), new Service()
}),
new Option("Section", new ObservableCollection<object>
{
new Section(), new Section(), new Section()
}),
};
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Classes I created
public class Option
{
public string Name { get; set; }
public ObservableCollection<object> Items { get; set; }
public Option(string name, ObservableCollection<object> items)
{
Name = name;
Items = items;
}
}
public class Service
{
}
public class Section
{
}
public class Contract
{
}
Hope some of this helps. Let me know if you have any questions.

Validating ItemsControl in wpf didn't work

I have an ItemsControl to display translations text fields.
I want to setup validating, so if all translations are empty, there was an error, and fields was marked as "error".
Is there any possibilty to do this?
My xaml:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}"
LostFocus="OnLostFocus" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center"
Text="{Binding Path=Key, StringFormat={x:Static res:Resources.lblCaption}}" />
<TextBox Grid.Column="1" x:Name="ItemText" VerticalAlignment="Center"
HorizontalAlignment="Stretch" Margin="2,0,22,0"
Text="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"
LostFocus="OnLostFocus"
AcceptsReturn="True"
MaxLines="2"
ScrollViewer.VerticalScrollBarVisibility="Auto"
MaxLength="150">
</TextBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My model's class implements from IDataErrorInfo and INotifyPropertyChanged
Translations is an ObservableCollection of custom type "LanguageValue" with public properties Key and Value.
I had in my model string this[string columnName], which works perfect with simple text boxes (outside ItemsControl), but how can make this works with my items? I've thight something like:
public string this[string columnName]
{
get
{
string result = null;
...
if (columnName == "Translations" || columnName == "ItemText")
{
if (Translations.All(t => string.IsNullOrEmpty(t.Value)))
result = Properties.Resources.errMsgEnterName;
}
...
But of course this didn't work.
Any suggestions?
Sure, I'm giving you a full implementation but with just the "Value" property. Do the same with all other properties that you want to validate:
1.Translation Model with IDataErrorInfo interface implementation:
public class Translation : BindableBase, IDataErrorInfo
{
public string Value { get; set; }
public string this[string propertyName]
{
get
{
return GetErrorForPropery(propertyName);
}
}
public string Error { get; }
private string GetErrorForPropery(string propertyName)
{
switch (propertyName)
{
case "Value":
if (string.IsNullOrEmpty(Value))
{
return "Please enter value";
}
return string.Empty;
default:
return string.Empty;
}
}
}
2.Initialize Translations in your ViewModel:
public ObservableCollection<Translation> Translations { get; set; }
public MainViewModel()
{
Translations = new ObservableCollection<Translation>
{
new Translation {Value = "A"},
new Translation (),
new Translation {Value = "C"}
};
}
3.Xaml with ValidatesOnDataErrors on the Value TextBox:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
4.That will show a red box around the empty TextBox, if you want to display the error messeage when hovering overthe TextBox you need a tool tip:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:customControlLib="clr-namespace:CustomControlLib;assembly=CustomControlLib"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow"
DataContext="{StaticResource MainViewModel}">
<Window.Resources>
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" Style="{StaticResource TextBoxStyle}" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

How to use a Binding of the ViewModel instead of the DataType of a ListView?

Let me begin with apologizing for the perhaps somewhat vague title, I'm having a hard time explaining my problem! Perhaps this is why I'm hardly getting any Google results, with this post being the closest to my problem (I think): How to bind to a property of the ViewModel from within a GridView
Anyways, I have a list of news articles that are being dynamically generated. The user has the option to press a "Star"-button in order to add an article to his/her favorites list. This "Star"-button should thus only be visible when the user is logged in.
I'm trying to achieve this by setting the Visibility of the Button to a property called IsLoggedIn inside my ViewModel. However, because this is happening inside my ListView it's trying to find the property IsLoggedIn inside of Article instead of the ViewModel.
So I guess my questions boils down to: How can I bind to a ViewModel property inside of a Databound ListView?
<ListView ItemsSource="{x:Bind VM.Articles, Mode=OneWay}" ItemClick="DebuggableListView_ItemClick" IsItemClickEnabled="True" SelectionMode="None" Grid.Row="1" VerticalAlignment="Top">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Article">
<Grid Margin="0,10,10,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="4*"/>
</Grid.ColumnDefinitions>
<Image Source="{Binding Image}" Margin="0,0,10,0" Grid.Column="0" VerticalAlignment="Top" ImageFailed="Image_ImageFailed"/>
<Button Visibility="{x:Bind VM.IsLoggedIn, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}" FontFamily="Segoe MDL2 Assets" Content="" FontSize="30" Background="Transparent" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Bottom"/>
<StackPanel Grid.Column="1">
<TextBlock TextWrapping="Wrap" FontWeight="Bold" Text="{Binding Title}"/>
<TextBlock TextWrapping="Wrap" Text="{Binding Summary}"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Requested Article class:
public sealed class Article
{
public int Id { get; set; }
public int Feed { get; set; }
public string Title { get; set; }
public string Summary { get; set; }
public DateTime PublishDate { get; set; }
public string Image { get; set; }
public string Url { get; set; }
public string[] Related { get; set; }
public Category[] Categories { get; set; }
public bool IsLiked { get; set; }
}
Okay, so currently I got it working by having a property which gets the singleton of my VM, however I'm sure there has to be a cleaner way to get something simple like this working. I've added a sample rar (OneDrive link: https://1drv.ms/u/s!Ar4fOTiwmGYnyWbwRY6rM0eFsL9x) which has a list, a ViewModel, some dummy data and a Visibility property inside the VM. If you can get it working without my dirty method please feel free to commit as an answer.
This type problem can best be solved using relative binding. Rather than binding directly to the current DataContext, i.e. this case the individual item in the ListView's ItemsSource, you can bind to a property of any ancestor element.
Given a simple ViewModel class
public class ViewModel: ViewModelBase
{
private bool _isLoggedIn;
public bool IsLoggedIn
{
get { return _isLoggedIn; }
set
{
_isLoggedIn = value;
RaisePropertyChanged(() => IsLoggedIn);
}
}
public IEnumerable<string> Items
{ get { return new[] {"One", "Two", "Theee", "Four", "Five"}; } }
}
the View can then be defined as
<Window x:Class="ParentBindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ParentBindingTest"
Title="MainWindow"
Width="300" Height="400">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<CheckBox Margin="16,8" HorizontalAlignment="Left" Content="Logged In?" IsChecked="{Binding IsLoggedIn, Mode=TwoWay}" />
<ListView Grid.Row="1" Margin="16,8" ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Visibility="{Binding Path=DataContext.IsLoggedIn, RelativeSource={RelativeSource AncestorType={x:Type ListView}}, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image Source="Images\remove.png" />
</Button>
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Window>
Note how in the item DataTemplate, the TextBlock's Text property is bound directly to the item.
However the Button's Visibility is bound not to a propery of it's own DataContext, but to that of the ListView itself, using relative binding.

Recursive binding in WPF with templates

So I have a custom class that is designed to contain menu items and sub menu items formatted as such:
public class ApplicationMenuItem
{
public ImageSource Image { get; set; }
public string Text { get; set; }
public string Tooltip { get; set; }
public ICollection<ApplicationMenuItem> Items { get; set; }
public EventHandler Clicked {get;set;}
public void Click(object sender, EventArgs e)
{
if (Clicked!=null)
Clicked(this, e);
}
public ApplicationMenuItem(string Text)
{
this.Text = Text;
Items = new List<ApplicationMenuItem>();
}
public ApplicationMenuItem()
{
Items = new List<ApplicationMenuItem>();
}
}
Before anybody asks why I don't inherit Menu or just create a Menu object and bind it, its because this class may be used on platforms and frameworks that don't necessarily use the Menu UI object, not to mention this class will drive nav menus, context menus, sidebars, toolbars etc....
My question is as you can see I have a self referencing list Items contained within to allow sub menus; binding the first level menu elements is easy enough, but how do I recursively bind sub elements while creating a template for its elements in WPF?
Here's an example of a recursive XAML template using your ApplicationMenuItem class exactly as you defined it (except that I put it in a namespace called Wobbles). This is not finished, releasable code. But it demonstrates a recursive DataTemplate, and some bonus goodies like displaying the popup. You can add an IsEnabled property to your menu item class and implement it in the XAML with additional trigger that sets colors, and an additional condition in the multitrigger that drives SubmenuPopup.IsOpen. If you want to support horizontal separators, you could add a property bool ApplicationMenuItem.IsSeparator and give the template a trigger which replaces the grid content below with a horizontal line when that property is True.
RecursiveTemplate.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wbl="clr-namespace:Wobbles"
>
<DataTemplate DataType="{x:Type wbl:ApplicationMenuItem}">
<Grid
Name="RootGrid"
Background="BlanchedAlmond"
Height="Auto"
UseLayoutRounding="True"
SnapsToDevicePixels="True"
>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="24" />
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Source="{Binding Image}"
/>
<Label
Grid.Column="1"
Content="{Binding Text}"
/>
<Border
Name="PopupGlyphBorder"
Grid.Column="2"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Background="{Binding ElementName=RootGrid, Path=Background}"
>
<Path
Height="10"
Width="5"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Data="M 0,0 L 5,5 L 0,10 Z"
Fill="Black"
/>
</Border>
<Popup
Name="SubmenuPopup"
PlacementTarget="{Binding ElementName=PopupGlyphBorder}"
Placement="Right"
StaysOpen="True"
>
<Border
BorderBrush="DarkGoldenrod"
BorderThickness="1"
>
<ItemsControl
Name="SubmenuItems"
ItemsSource="{Binding Items}"
/>
</Border>
</Popup>
</Grid>
<DataTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RootGrid" Property="Background" Value="Wheat" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition SourceName="SubmenuItems" Property="HasItems" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="SubmenuPopup" Property="IsOpen" Value="True" />
</MultiTrigger>
<Trigger SourceName="SubmenuItems" Property="HasItems" Value="False">
<Setter TargetName="PopupGlyphBorder" Property="Visibility" Value="Hidden" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ResourceDictionary>
MainWindow.xaml
<Window
x:Class="RecursiveTemplate.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wbl="clr-namespace:Wobbles"
Title="MainWindow"
Height="350"
Width="525"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="RecursiveTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.DataContext>
<wbl:TestViewModel />
</Window.DataContext>
<Grid>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<ContentControl
Content="{Binding Menu}"
Width="100"
Height="24"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
ViewModel.cs
namespace Wobbles
{
public class TestViewModel
{
public TestViewModel()
{
Menu = CreateMenu();
}
public Wobbles.ApplicationMenuItem Menu { get; protected set; }
protected Wobbles.ApplicationMenuItem CreateMenu()
{
var m = new Wobbles.ApplicationMenuItem("Menu");
var msub = new Wobbles.ApplicationMenuItem("Submenu");
msub.Items.Add(new Wobbles.ApplicationMenuItem("Sub Sub 1"));
msub.Items.Add(new Wobbles.ApplicationMenuItem("Sub Sub 2"));
// LOL
msub.Items.Add(msub);
m.Items.Add(msub);
m.Items.Add(new Wobbles.ApplicationMenuItem("Foo"));
m.Items.Add(new Wobbles.ApplicationMenuItem("Bar"));
m.Items.Add(new Wobbles.ApplicationMenuItem("Baz"));
return m;
}
}
}
Nits, Cavils, Kvetches, and a Brief Homily
Working with XAML, I'd suggest making a practice of using ObservableCollection<T> instead of List<T>. If the items in the collection change after the UI is constructed, ObservableCollection<T> will cause the UI to update appropriately. For the same reason, you'll want ApplicationMenuItem to implement INotifyPropertyChanged. I'd also prefer supporting an ICommand Command property as well as the Click event, and I'd further name the Click event Click in accordance with standard XAML practice.
"What Would XAML Do?" You'll pretty much never go wrong if you do your utmost to write code that could be mistaken for the standard library that shipped with the environment you're working in.

Categories