Display Items Using Grid in Xamarin Forms - c#

I am working on Xamarin Forms(Android,IOS,Windows).
I trying to display items in a grid with selection (highlight the selected item).
Please find the below image for more information.
Can any one suggest me, How to achieve items display in grid with selection?

Generally each item binds a model which contains a boolean value that keeps selected or not. With that approach you ca initialize them as you want. Then create Gesture recognizer for tapping gesture & use Trigger for selection effect(BackgroundColor)
(In your definiton you mentioned to using inside Grid, so I'm not offering new methodology for UI hierarchy)
Here is theCode:
Model:
public Class ItemModel: INotifyPropertyChanged
{
// implement INotifyPropertyChanged interface
public ItemModel()
{
ToggleCommand = new Command(CmdToggle);
}
private void CmdToggle()
{
IsSelected = !IsSelected;
}
public string Name
{
get;
set; //call OnPropertyChanged
}
public bool IsSelected
{
get;
set; //call OnPropertyChanged
}
public ICommand ToggleCommand{get;private set;}
}
ViewModel
public Class PageViewModel: INotifyPropertyChanged
{
public List<ItemModel> Items
{
get;
set; //call OnPropertyChanged
}
}
Converter
public class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is bool)
return ((bool)value) ? Color.Gray: Color.White;
else
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Xaml:
<Page.Resources>
<Color x:Key="SelectedColor">Gray</Color/>
<Color x:Key="UnselectedColor">White</Color/>
<namespace:BoolToColorConverter x:Key="BoolToColorConverter"/>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!--Single item -->
<StackLayout Grid.Row="0"
Grid.Column="0"
BindingContext="{Binding Items[0]}"
Orientation="Vertical"
BackgroundColor="{Binding IsSelected,Converter={StaticResource BoolToColorConverter}}"
>
<Image Source="{Binding yourImageProperty}" />
<Image Source="{Binding yourImage2Property}" />
<Label Source="{Binding Name}"/>
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ToggleCommand}" />
</StackLayout.GestureRecognizers>
<!--Triggers will update background color when update IsSelected-->
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout" Binding="{Binding IsSelected}" Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource SelectedColor}" />
</DataTrigger>
<DataTrigger TargetType="c:Label" Binding="{Binding IsSelected}" Value="False">
<Setter Property="BackgroundColor" Value="{StaticResource UnselectedColor}" />
</DataTrigger>
</StackLayout.Triggers>
</StackLayout>
</Grid>

Related

How to get list of Controls of a type from whole Window?

I have WPF MVVM application where I have several ItemsControls on several levels looking like:
<ItemsControl Grid.Column="1" ItemsSource="{Binding ElementName=cb_Letter, Path=SelectedItem.Words2}" Name="ICWords2">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="8*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding .}" />
<TextBox Grid.Column="1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
and I want to controll all items by command executed from:
<Button Grid.Column="4" Name="btn_Check" Command="CMDS:Commands.Check"></Button>
In command:
var parrents = CollectionOfAll<ItemsControl>(Window);
foreach (var items in parrents)
{
foreach (var item in items.Items)
{
var child = FindChild<Grid>((UIElement)items.ItemContainerGenerator.ContainerFromItem(item));
Label lb = child.Children.OfType<Label>().First();
TextBox tb = child.Children.OfType<TextBox>().First();
tb.BorderBrush = lb.Content.ToString() == tb.Text.ToString() ? Brushes.Green : Brushes.Red;
}
}
But I am brand new to WPF and cant figure out how to get all childs of childs of childs .... and so on of type
so TODO is:
private List<T> CollectionOfAll<T>(...){...}
Why am I doing it like this?
It is application for injured kids to learn how to write on pc, so in label is a character or string and they have to try write it to textbox on the right of that label and then I have to chek if it is correct
Manually parsing controls in a WPF application is an antipattern.
WPF provides better ways to achieve what you need through binding and templating.
You can use a Style on each TextBox to have WPF automatically change the BorderBrush property depending on the equality result between the Text property and the DataContext value (which seems to be a of type string in your case).
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.Resources>
<local:EqualityConverter x:Key="EqualityConverter"/>
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="8*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding .}" />
<TextBox x:Name="textBox" Grid.Column="1">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="Red" /> <!-- Default color -->
<Style.Triggers>
<DataTrigger Value="true">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource EqualityConverter}">
<Binding RelativeSource="{RelativeSource Mode=Self}" Path="Text"/>
<Binding Path="."/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="BorderBrush" Value="Green" /> <!-- Color when values are equal -->
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You need to define an additional piece of code (a Converter):
/// <summary>
/// Returns true if all passed values are equal.
/// </summary>
public class EqualityConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
=> values.All(v => v.Equals(values[0]));
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
Fixing the issue in a WPF-oriented way
An even better way to achieve what you want is to define a correct view model from the start:
public class QuestionViewModel {
public string RequiredText { get; set; }
public string InputText{ get; set; }
public bool IsCorrect => RequiredText.Equals(InputText);
}
(It should implement INotifyPropertyChanged but for simplicity I haven't shown that)
And you would bind your ItemsControl.ItemsSource to a collection of QuestionViewModel instead of a collection of string:
<ItemsControl ItemsSource="{Binding Questions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="8*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding RequiredText}" />
<TextBox x:Name="textBox" Grid.Column="1" Text="{Binding InputText}">
<TextBox.Style>
<Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="Red" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsCorrect}" Value="true">
<Setter Property="BorderBrush" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You should have a look at the MVVM pattern probably.

wpf bind to selected listview item and update another list based on that selection

I'm using Visual Studio 2015, I'm trying to teach myself the MVVM pattern, and I'm hitting a road block. My code is loosely based off of Josh Smiths article, I'm using it to help me learn MVVM and create a small app for work in the process.
What I'm trying to accomplish:
I've bound a viewmodel to a listview showing a list of products, each product has a list of "productTemplate" items. In my View I would like this list to populate inside a listbox when a product from my list view is selected. I am implementing INotifyPropertyChanged. I think I'm just missing something simple but I'm not sure.
My code:
Two Models (Product, ProductTemplateItem);
public class Product {
private string _productNum;
private string _productFamily;
public Product() {
}
public string ProductNum { get; set; }
public string ProductFamily { get; set; }
}
public class ProductTemplateItem : ChangeEventHandlerBase {
private string _TemplateItem;
private string _TemplateCode;
public ProductTemplateItem(string templateItem, string templateCode) {
_TemplateItem = templateItem;
_TemplateCode = templateCode;
}
public string TemplateItem {
get { return _TemplateItem; }
set { if(_TemplateItem != value) {
_TemplateItem = value;
OnPropertyChanged("TemplateItem");
}
}
}
public string TemplateCode {
get { return _TemplateCode; }
set {
if (_TemplateCode != value) {
_TemplateCode = value;
OnPropertyChanged("TemplateCode");
}
}
}
public override string DisplayName {
get {
return $"{TemplateItem} - {TemplateCode}";
}
}
}
My ViewModels (Product View Model, brings everything together and adds the product template list, and AlProductsViewModel adds data and exposes everything to be bound in XAML):
public class ProductViewModel : BaseViewModel {
private Product _product;
private bool _isSelected;
private List<ProductTemplateItem> _productTemplate;
public ProductViewModel(string productNum, string productFamily) {
Product.ProductNum = productNum;
Product.ProductFamily = productFamily;
_productTemplate = new List<ProductTemplateItem>();
}
public string ProductNumber {
get { return _product.ProductNum; }
set { if(_product.ProductNum != value) {
_product.ProductNum = value;
OnPropertyChanged("ProductNumber");
}
}
}
public string ProductFamily {
get { return _product.ProductFamily; }
set {
if (_product.ProductFamily != value) {
_product.ProductFamily = value;
OnPropertyChanged("ProductFamily");
}
}
}
public bool IsSelected {
get { return _isSelected; }
set {
if (_isSelected != value) {
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public List<ProductTemplateItem> ProductTemplate {
get { return _productTemplate; }
set { if (_productTemplate != value) {
_productTemplate = value;
OnPropertyChanged("ProductTemplate");
}
}
}
public Product Product {
get {
if (_product == null) {
_product = new Product();
return _product;
}
else {
return _product;
}
}
set { if(_product != value) {
_product = value;
OnPropertyChanged("Product");
}
}
}
public override string DisplayName {
get {
return Product.ProductNum;
}
}
}
public class AllProductsViewModel : BaseViewModel{
public AllProductsViewModel() {
AddProducts();
}
private void AddProducts() {
List<ProductViewModel> all = new List<ProductViewModel>();
all.Add(new ProductViewModel("4835", "Crop Cart"));
all.Add(new ProductViewModel("780", "Piler"));
all.Add(new ProductViewModel("880", "Piler"));
all.Add(new ProductViewModel("150", "Scooper"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Miscellaneous","MISC"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Drawbar", "DRBR"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Mainframe", "FRAM"));
all[0].ProductTemplate.Add(new Model.ProductTemplateItem("Conveyor", "CONV"));
all[1].ProductTemplate.Add(new Model.ProductTemplateItem("Hello", "HELL"));
AllProducts = new ObservableCollection<ProductViewModel>(all);
}
public ObservableCollection<ProductViewModel> AllProducts { get; private set; }
}
And my XAML code which is a user control with a ListView based off of Josh's code and a listbox that needs to be updated based off of the selection in the ListView:
<UserControl x:Class="Parts_Book_Tool.Views.ProductsView"
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:Parts_Book_Tool.Views"
xmlns:viewModel="clr-namespace:Parts_Book_Tool.ViewModel"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<viewModel:AllProductsViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<CollectionViewSource x:Key="ProductGroups" Source="{Binding Path=AllProducts}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ProductFamily"/>
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="ProductFamily" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<GroupStyle x:Key="ProductGroupStyle">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock
FontWeight="Bold"
Margin="1"
Padding="4,2,0,2"
Text="{Binding Path=Name}"
/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
<Style x:Key="MainHCCStyle" TargetType="{x:Type HeaderedContentControl}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Border
Background="{StaticResource Brush_HeaderBackground}"
BorderBrush="LightGray"
BorderThickness="1"
CornerRadius="5"
Margin="4"
Padding="4"
SnapsToDevicePixels="True"
>
<TextBlock
FontSize="14"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
Text="{TemplateBinding Content}"
/>
</Border>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="ProductItemsStyle" TargetType="{x:Type ListViewItem}">
<!--
Stretch the content of each cell so that we can
right-align text in the Total Sales column.
-->
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<!--
Bind the IsSelected property of a ListViewItem to the
IsSelected property of a CustomerViewModel object.
-->
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ItemsControl.AlternationIndex" Value="1" />
<Condition Property="IsSelected" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#EEEEEEEE" />
</MultiTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<HeaderedContentControl Header="Model Info" Style="{StaticResource MainHCCStyle}" Grid.Row="0">
<ListView x:Name="lvModelNumbers" Margin="6,2,6,50" DataContext="{StaticResource ProductGroups}"
ItemContainerStyle="{StaticResource ProductItemsStyle}" ItemsSource="{Binding}" >
<ListView.GroupStyle>
<StaticResourceExtension ResourceKey="ProductGroupStyle"/>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Model Number" Width="100" DisplayMemberBinding="{Binding Path=DisplayName}"/>
</GridView>
</ListView.View>
</ListView>
</HeaderedContentControl>
<HeaderedContentControl Header="Model Template" Style="{StaticResource MainHCCStyle}" Grid.Row="1">
<ListBox ItemsSource="{Binding SelectedItem/ProductTemplate, ElementName=lvModelNumbers}" Margin="6,2,6,50">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</HeaderedContentControl>
</Grid>
It feels to me like I'm missing the capture of an event to update the listbox, but with my inexperience in MVVM I Can't be sure. I've tried binding to the SelectedItem of the named element but that doesn't work. I can get the listbox to populate if I bind "AllProducts/ProductTemplate", but it just gives me the first indexed values, and doesn't dynamically change when I select another product.
Hopefully that is enough information, and any help would be greatly appreciated. I'm enjoying learning MVVM but it's dificult to wrap my head around.
Thanks,
Try bind the ListBox to the ProductTemplate property of the SelectedItem property of the ListView:
<ListView x:Name="lvModelNumbers" Margin="6,2,6,50"
ItemsSource="{Binding Source={StaticResource ProductGroups}}"
ItemContainerStyle="{StaticResource ProductItemsStyle}">
<ListView.GroupStyle>
<StaticResourceExtension ResourceKey="ProductGroupStyle"/>
</ListView.GroupStyle>
<ListView.View>
<GridView>
<GridViewColumn Header="Model Number" Width="100" DisplayMemberBinding="{Binding Path=DisplayName}"/>
</GridView>
</ListView.View>
</ListView>
<ListBox ItemsSource="{Binding SelectedItem.ProductTemplate, ElementName=lvModelNumbers}" Margin="6,2,6,50">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

WPF Binding not updating Visibility

I'm having trouble with the binding to a visibility of a grid. I've had projects where I've done this before and have tried to replicate the same coding used previously, and I've searched around and added in the bool to visibility converter based off some other articles but still nothing.
All that I am trying to do so far in this project is have a set of options that will be provided to the user. When they select one option it will either bring up sub-options or just take them to the proper area. I have the binding set to a bool object and have at times created little message boxes and others just to let me know if the program is reaching everywhere. I get every messagebox along the way, so it appears to be reaching every piece of code.
Can anyone shed some light on what I am doing wrong or point me in the correct direction?
Converter in Windows.Resources (Edited to show all code in Windows.Resources)
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Height" Value="50" />
<Setter Property="Width" Value="100" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
Code in the rest of the window
<Grid>
<Grid x:Name="grid_mainMenu" Visibility="{Binding MainMenuVisibility, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="0,0,0,20">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Button x:Name="button_Items" Content="Items" Grid.Row="0" Click="button_Items_Click"/>
<Button x:Name="button_Orders" Content="Orders" Grid.Row="1" Click="button_Orders_Click" />
<TextBox Text="{Binding StatusMessage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="100" Width="100"/>
</Grid>
<Grid x:Name="grid_itemMenu" Visibility="{Binding ItemMenuVisibility, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="0,0,0,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Label Content="Item Menu" Grid.Row="0" FontSize="20" FontWeight="Bold" Margin="0,0,0,0" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Button Grid.Row="1" x:Name="button_itemMaintenance" Content="Maintenance"/>
<Button Grid.Row="2" x:Name="button_itemCreation" Content="Create"/>
</Grid>
<DockPanel Height="25" Margin="0,0,0,0" VerticalAlignment="Bottom">
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock Text="{Binding StatusMessage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StatusBarItem>
</StatusBar>
</DockPanel>
</Grid>
Here is the code in the class
public bool MainMenuVisibility
{
get { return _mainMenuVisibility; }
set { _mainMenuVisibility = value; RaisePropertyChanged(); }
}
public bool ItemMenuVisibility
{
get { return _itemMenuVisibility; }
set
{ _itemMenuVisibility = value; RaisePropertyChanged(); }
}
public bool OrderMenuVisibility
{
get { return _orderMenuVisibility; }
set { _orderMenuVisibility = value; RaisePropertyChanged(); }
}
Main Constructor
public Menu_View()
{
ShowMainMenu();
}
A couple of the controls
public void ShowMainMenu()
{
MainMenuVisibility = true;
HideItemMenu();
HideOrderMenu();
StatusMessage = "Showing main menu";
}
public void HideMainMenu()
{
MainMenuVisibility = false;
StatusMessage = "Hid main menu";
}
public void ShowItemMenu()
{
try
{
//Reaches, but never updates
ItemMenuVisibility = true;
HideMainMenu();
HideOrderMenu();
}
catch(Exception error)
{
//Never shows anything here
StatusMessage = "Failed to load item menu";
}
finally
{
//Does not update, but reaches here
StatusMessage = "Showing item menu";
}
}
Program starts by showing the main menu, when user clicks a button for Items it is supposed to show the item menu. The button click calls ShowItemMenu(). I have verified that that does happen and is called in proper order.
I have verified that ShowItemMenu() does work but putting in the constructor instead of ShowMainMenu(). Either one works fine, but neither will cause an update after the initial loading even though they are reached after button presses.
Sorry if I did not include everything I needed.
EDIT:
I believe that I actually had two issues going on simultaneously. One an improperly configured data converter. Answer and reference below.
As well as an issue in my window code here:
public MainWindow()
{
InitializeComponent();
menuView = new Menu_View();
this.DataContext = new Menu_View();
}
Menu_View menuView;
I believe this was part of the issue. I was creating a menuView of type Menu_View. On initialize I assigned menuView to a new Menu_View() and then assigned my DataContext to a new Menu_View() instead of menuView. All commands were updating menuView and I was updating the one assigned to the DataContext.
Add this converter class to your project.
class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Boolean && (bool)value)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Visibility && (Visibility)value == Visibility.Visible)
{
return true;
}
return false;
}
}
Referance
You could go without the Boolean converter if you choose. You won't have to write any code-behind.
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MyBoolValue}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>

How to change the DataTemplate of a custom type based on a ViewModel property?

I'd like to be able to change the DataTemplate that my custom class is using, based on a property in the ViewModel.
I can't find any clear examples and I feel like I might not know enough about WPF or XAML to know if this is even possible.
My ViewModel property represents whether the user has collapsed a column on one side of the application. If the column is collapsed, I want to only show the Image for each user, and if the column is expanded, I'll show the picture, first name and last name in a StackPanel.
I feel like there is something really basic that I just don't understand yet and I guess I'm looking for someone who maybe tried something like this or knows how to do this the right way.
User.cs
public class User
{
public string ImageFile {get; set;}
public string FirstName {get; set;}
public string LastName {get; set;}
}
I'm using an ObservableCollection<User> to hold my collection of User objects in the viewmodel.
My 2 DataTemplates that I'd like to use. (right now I'm just using a default image and text to see how it looks)
DataTemplates
<DataTemplate x:Key="UserCollapsed">
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
</DataTemplate>
<DataTemplate x:Key="UserExpanded">
<StackPanel>
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
<TextBlock Text="Firstname"/>
<TextBlock Text="Lastnamehere"/>
</StackPanel>
</DataTemplate>
I've tried to write a style, and apply that to my ItemsControl in the view, and I've tried writing a datatemplate that uses triggers to decide which template to use, but I can't quite figure out where I'm going wrong.
Style
<Style x:Key="userTemplateStyle" TargetType="ItemsControl">
<Setter Property="ItemTemplate" Value="{StaticResource UserExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="ItemTemplate" Value="{StaticResource UserCollapsed}"/>
</DataTrigger>
</Style.Triggers>
</Style>
I get the following exception when I add the Style property on my ItemsControl in XAML.
Exception
{"Unable to cast object of type 'MS.Internal.NamedObject' to type 'System.Windows.DataTemplate'."}
And the DataTemplate that I tried to use as the ItemTemplate of the ItemsControl. (I feel like this is the wrong way to go about it but I tried anyway)
DataTemplate
<DataTemplate DataType="{x:Type md:CUser}">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="DataTemplate" Value="{StaticResource UserCollapsed}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
ItemsControl
<ItemsControl Visibility="{Binding ColumnVisibility}"
Style="{StaticResource userTemplateStyle}"
BorderThickness="0"
Name="itcLoggedInUsers"
Margin="0"
ItemsSource="{Binding LoggedInUsers}"
Grid.Row="1"/>
Your code works fine for me...
<Window x:Class="WpfApplication8.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WpfApplication8="clr-namespace:WpfApplication8"
Title="MainWindow"
Width="525"
Height="350">
<Window.Resources>
<WpfApplication8:ViewModel x:Key="ViewModel" />
<DataTemplate x:Key="UserCollapsed" />
<DataTemplate x:Key="UserExpanded">
<StackPanel Width="200"
Height="200"
Background="Red">
<TextBlock Text="Firstname" />
<TextBlock Text="Lastnamehere" />
</StackPanel>
</DataTemplate>
<Style x:Key="userTemplateStyle" TargetType="ItemsControl">
<Setter Property="ItemTemplate" Value="{StaticResource UserExpanded}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel}}" Value="True">
<Setter Property="ItemTemplate" Value="{StaticResource UserCollapsed}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid x:Name="grid" DataContext="{StaticResource ViewModel}">
<ItemsControl Name="itcLoggedInUsers"
Grid.Row="1"
Margin="0"
BorderThickness="0"
ItemsSource="{Binding LoggedInUsers}"
Style="{StaticResource userTemplateStyle}" />
</Grid>
And the ViewModel
public class ViewModel: INotifyPropertyChanged
{
private bool _columnIsCollapsed;
public ViewModel()
{
ColumnIsCollapsed = false;
LoggedInUsers = new ObservableCollection<User>();
LoggedInUsers.Add(new User(){FirstName = "SSSSSS", LastName = "XXXXXX"});
}
public bool ColumnIsCollapsed
{
get { return _columnIsCollapsed; }
set
{
_columnIsCollapsed = value;
OnPropertyChanged(new PropertyChangedEventArgs("ColumnIsCollapsed"));
}
}
public ObservableCollection<User> LoggedInUsers { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
public class User
{
public string ImageFile { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
I over thought this problem. If I needed to show or hide an element in the DataTemplate based on a boolean property of the ViewModel, I could just bind the visibility of the element to the property on the ViewModel, and use a converter to return a Visibility.
Solution
<DataTemplate DataType="{x:Type md:User}">
<StackPanel>
<Image Source="/Images/anon.png"
Height="50"
Width="50"
Margin="0,5,0,0"/>
<TextBlock Text="Firstname" Visibility="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel},Converter={StaticResource InvertBoolVisibility}}"/>
<TextBlock Text="Lastnamehere" Visibility="{Binding ColumnIsCollapsed, Source={StaticResource ViewModel},Converter={StaticResource InvertBoolVisibility}}"/>
</StackPanel>
</DataTemplate>
Converter
public class InvertBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var theBool = (bool)value;
if (theBool)
return Visibility.Collapsed;
else
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

Why does my ConvertBack not getting called, WPF Converter & ValidationRule?

I'm trying to bind a textbox which can validate email addresses split by ',' or ';'.The objective is to have a checkbox, a textbox and a button on the page.If the checkbox is checked, then the user has to enter a valid email address or else the button has to be disabled.If the checdkbox is not clicked then the button has to be enabled.I'm going round in circles with this one, could you please help?
Please find my ViewModel structure below:
public class EmailValidatorViewModel : DependencyObject
{
public EmailValidatorViewModel()
{
OnOkCommand = new DelegateCommand<object>(vm => OnOk(), vm => CanEnable());
OtherRecipients = new List<string>();
}
private bool CanEnable()
{
return !IsChecked || HasOtherRecipients() ;
}
public static readonly DependencyProperty OtherRecipientsProperty =
DependencyProperty.Register("OtherRecipients", typeof(List<string>), typeof(EmailValidatorViewModel));
public List<string> OtherRecipients
{
get { return (List<string>)GetValue(OtherRecipientsProperty); }
set
{
SetValue(OtherRecipientsProperty, value);
}
}
public bool IsChecked { get; set; }
public void OnOk()
{
var count = OtherRecipients.Count;
}
public bool HasOtherRecipients()
{
return OtherRecipients.Count != 0;
}
public DelegateCommand<object> OnOkCommand { get; set; }
}
Also below is my XAML page:
<Window x:Class="EMailValidator.EMailValidatorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:EMailValidator="clr-namespace:EMailValidator" Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style x:Key="ToolTipBound" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Text" Value=""/>
</Trigger>
</Style.Triggers>
</Style>
<EMailValidator:ListToStringConverter x:Key="ListToStringConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Margin="15,0,0,0" x:Name="OthersCheckbox" Grid.Row="2" Grid.Column="0" Unchecked="OnOthersCheckboxUnChecked" IsChecked="{Binding Path=IsChecked}">Others</CheckBox>
<TextBox Margin="5,0,5,0" x:Name="OtherRecipientsTextBox" Grid.Row="2" Grid.Column="1" IsEnabled="{Binding ElementName=OthersCheckbox, Path=IsChecked}" Style="{StaticResource ToolTipBound}">
<TextBox.Text>
<Binding Path="OtherRecipients" Mode="TwoWay" Converter="{StaticResource ListToStringConverter}" NotifyOnSourceUpdated="True">
<Binding.ValidationRules>
<EMailValidator:EmailValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="OkButton" Grid.Row="3" Grid.Column="0" Command="{Binding OnOkCommand}">Ok</Button>
</Grid>
</Window>
Also I do set my datacontext as below in the constructor of my xaml page.
public EMailValidatorWindow()
{
InitializeComponent();
DataContext = new EmailValidatorViewModel();
}
Here is my EMail Validator:
public class EmailValidationRule:ValidationRule
{
private const string EmailRegEx = #"\b[A-Z0-9._%-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\b";
private readonly Regex regEx = new Regex(EmailRegEx,RegexOptions.IgnoreCase);
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var inputAddresses = value as string;
if(inputAddresses == null)
return new ValidationResult(false,"An unspecified error occured while validating the input.Are you sure that you have entered a valid EMail address?");
var list = inputAddresses.Split(new[] {';',','});
var failures = list.Where(item => !regEx.Match(item).Success);
if(failures.Count() <= 0)
return new ValidationResult(true,null);
var getInvalidAddresses = string.Join(",", failures.ToArray());
return new ValidationResult(false,"The following E-mail addresses are not valid:"+getInvalidAddresses+". Are you sure that you have entered a valid address seperated by a semi-colon(;)?.");
}
...and my converter:
public class ListToStringConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var input = value as List<string>;
if (input.Count == 0)
return string.Empty;
var output = string.Join(";", input.ToArray());
return output;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var input = value as string;
if (string.IsNullOrEmpty(input))
return new List<string>();
var list = input.Split(new[] { ';' });
return list;
}
}
Try setting the UpdateSourceTrigger to 'PropertyChanged' in your textbox binding.
<Binding UpdateSourceTrigger="PropertyChanged" Path="OtherRecipients" Mode="TwoWay" Converter="{StaticResource ListToStringConverter}" NotifyOnSourceUpdated="True">

Categories

Resources