I have a combobox with names in it. I have the box set to editable so that the user can enter a name. I want it so that the user can only enter a name that is already in the list.
When the user clicks save I want the box to have the red validation border show up if the box is empty or not in the list.
Is there a way to do this?
<ComboBox IsEditable="True"
Grid.Column="2"
Grid.Row="1"
Margin="5,3,0,0"
Text="{Binding Model.Number}"
ItemsSource="{Binding DList}"
SelectedItem="{Binding Model.Number}"
IsEnabled="{Binding EnableComboBox}"
VerticalAlignment="Top">
</ComboBox>
If I understood correctly, you want the user to be able to select an existing list item by typing, but not type a string that is not on the list. That can be done with the following:
<ComboBox IsEditable="False"></ComboBox>
This will allow the user to start typing the string, but you lose the textbox for input.
Another way to do it is to allow the user to type whatever they want by setting <ComboBox IsReadOnly="False" IsEditable="True"> and handle for example the LostFocus event to check if the input is valid. Here's an example:
private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
{
bool allowed = false;
foreach (ComboBoxItem it in comboBox.Items)
{
if (it.Content.ToString() == comboBox.Text)
{
allowed = true;
break;
}
}
if (!allowed)
{
MessageBox.Show("MISS!");
}
else
{
MessageBox.Show("HIT!");
}
}
For some reason I wasn't able to set the border color quickly, but you get the point from here. Also depending on your ComboBoxItem type, you may need to match the comboBox.Text to a certain property.
Let's assume you use MVVM (it's not what you're doing now) and that
ItemsSource="{Binding DList}"
is a correct binding to a collection of Models
You'd need a
DisplayMemberPath="Number"
Back to your question.
First, let's write another binding for the selected Text
Text="{Binding Selected, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors =true, NotifyOnValidationError=true}"
and implement the validation tooltip inside the combo
ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
and the style in the window resources
<Window.Resources>
<Style TargetType="{x:Type Label}">
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="HorizontalAlignment" Value="Right" />
</Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Margin" Value="0,2,40,2" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="true">
<Border Background="Red" DockPanel.Dock="right" Margin="5,0,0,0" Width="20" Height="20" CornerRadius="10"
ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
<TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white">
</TextBlock>
</Border>
<AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
<Border BorderBrush="red" BorderThickness="1" />
</AdornedElementPlaceholder>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
Finally, we've to validate the ViewModel property
We can project the model list to its numbers for the error check
public class VM : IDataErrorInfo
{
public string this[string columnName]
{
get
{
if (columnName.Equals( "Selected"))
{
if (!DList.Select(m => m.Number).Contains(Selected))
return "Selected number must be in the combo list";
}
return null;
}
}
You can learn more about data validation in MVVM for example here
Fire the validation later
Say you want fire the validation after a save button is clicked
<Button Content="Save"
Command="{Binding SaveCmd}"
you simply need to raise the property changed in the corresponding delegate command
public class VM : ViewModelBase, IDataErrorInfo
{
private bool showValidation;
private int selected;
public int Selected
{
get { return selected; }
set
{
selected = value;
showValidation = true;
OnPropertyChanged("Selected");
}
}
DelegateCommand saveCmd;
public ICommand SaveCmd
{
get
{
if (saveCmd == null)
{
saveCmd = new DelegateCommand(_ => RunSaveCmd(), _ => CanSaveCmd());
}
return saveCmd;
}
}
private bool CanSaveCmd()
{
return true;
}
private void RunSaveCmd()
{
showValidation = true;
OnPropertyChanged("Selected");
}
and exit from the validation before you want to show it.
public string this[string columnName]
{
get
{
if (!showValidation)
{
return null;
}
Related
I'm using right now this:
<Window.Resources>
<DataTemplate x:Key="DataTemplate_Level2">
<Button Content="{Binding}" Height="40" Width="50" Margin="4,4,4,4"/>
</DataTemplate>
<DataTemplate x:Key="DataTemplate_Level1">
<ItemsControl ItemsSource="{Binding}" ItemTemplate="{DynamicResource DataTemplate_Level2}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</Window.Resources>
With this:
<ItemsControl
x:Name="lst"
ItemTemplate="{DynamicResource DataTemplate_Level1}"
ItemsSource="{Binding TopSeats}"/>
I'm binding a List< List< int?>> as itemssource with numbers 0-2. 0 for empty, 1 for selected, 2 for booked.
List<List<int?>> topSeats;
public List<List<int?>> TopSeats
{
get => topSeats;
set
{
topSeats = value;
NotifyPropertyChanged("TopSeats");
}
}
My UI looks like this right now:
enter image description here
When i press a button, it should change from 0 to 1, and the corresponding element in the List< List< int?>> container should change too.
But i've arrived to a brick wall. I have no idea how to make sure that, when i press any button, the correct element changes in the "List< List< int?>>" container.
Is it possible somehow without code behind?
Its a big process to explain everything here. But I'll try my best to give you a working solution and hope you can read more about INotifyPropertyChanged, MVVM pattern and ICommand pattern.
For simplicity, I have not implemented ICommand here and using a code-behind click to get the selected seat numbers (This is only for testing to see if selected seat numbers are able to retrieve or not).
Step 1: I have created a Model class called Seat with following properties and I am implementing INotifyPropertyChanged interface to capture property changed events. See below for my Seat.cs class.
public class Seat : INotifyPropertyChanged
{
private int seatNo;
private string seatNumber;
private bool isSelected;
public int SeatNo
{
get { return seatNo; }
set { seatNo = value; OnPropertyChanged(); }
}
public string SeatNumber
{
get { return seatNumber; }
set { seatNumber = value; OnPropertyChanged(); }
}
public bool IsSelected
{
get { return isSelected; }
set { isSelected = value; OnPropertyChanged(); }
}
public void OnPropertyChanged([CallerMemberName]string popertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(popertyName));
}
private void BaseVM_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
}
public event PropertyChangedEventHandler PropertyChanged;
}
Step 2:
I have modified "DataTemplate_Level2" template to have a CheckBox instead of a Button. Because I wanted to get the selected behavior where CheckBox has it. See below for the modified "DataTemplate_Level2"
<DataTemplate x:Key="DataTemplate_Level2" DataType="{x:Type local:Seat}">
<CheckBox Content="{Binding SeatNumber}" Height="40" Width="50" Margin="4" Style="{StaticResource CheckBoxStyle}"
IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
Step 3: I have modified appearance of the CheckBox. So that it does not appear like a checkbox but looks like a button (you can still customize to look like a real seat). See below for my modified CheckBoxStyle
<Style x:Key="CheckBoxStyle" TargetType="CheckBox">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Border x:Name="MainBorder" BorderBrush="Red" BorderThickness="1">
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=CheckBox}, Path=Content}"
TextAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="MainBorder" Property="Background" Value="Yellow" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Step 4: I have added some buttons and textblock to my Window to test for the selected seats (This piece of code is for testing purpose only). See below my rest of the xaml.
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ItemsControl x:Name="lst" ItemTemplate="{DynamicResource DataTemplate_Level1}" ItemsSource="{Binding TopSeatList}"/>
<Button Content="Get Selected Seat Numbers" Height="40" Width="50" Margin="4,4,4,4" Grid.Row="1" Click="Button_Click"/>
<TextBlock x:Name="SelectedSeatNumbersTextBlock" Grid.Row="2" />
</Grid>
Note:- There is no modification for "DataTemplate_Level1". Hence you can still copy-paste of yours.
Step 5: Now in my main window view model, I have added a list like your List< List > and populated some dummy data.
public List<List<Seat>> TopSeatList
{
get => topSeatList;
set
{
topSeatList = value;
OnPropertyChanged("TopSeatList");
}
}
Step 6: In the code-behind for the Button_Click event I did to get the selected seat numbers and display in a textblock.
private void Button_Click(object sender, RoutedEventArgs e)
{
var selectedSeats = selectSeatsViewModel.TopSeatList.SelectMany(x => x.Where(y => y.IsSelected));
string selectedSeatNumbers = string.Empty;
foreach(var seat in selectedSeats)
{
selectedSeatNumbers += seat.SeatNumber + "";
}
SelectedSeatNumbersTextBlock.Text = selectedSeatNumbers;
}
Note:- I consider to implement much better approach above click event with a command so that you can avoid it writing it in code-behind.
I hope this helps you to move forward with your solution. Please give a try and let us know results. Feel free to post your questions.
I need to be able to display lists of data in a ListBox with buttons that can move the items Up and Down and Remove items from the ListBoxes and reflect that in the data models.
SampleDesign: http://bigriverrubber.com/_uploads/sites/2/usercontrollistbox.jpg
I plan on having multiple ListBoxes just like this with the same functionality across several windows, so I thought I could make a UserControl with the ListBox and buttons I need inside it and have the buttons modify the data. That way I could just pass an ObservableCollection to the UserControl and I wouldn't have to recreate the buttons each time.
What I found out, however, is that I can't move the items if they are bound to an ObservableCollection, which they need to be for my purposes. From what I've read, I need to modify the collection instead.
But how do I do that from the UserControl? If the Type of the ObservableCollection needs to be variable so the ListBox can display many Types of lists, how can I possibly hope to target it to gain access to the Move and Remove methods in the ObservableCollection class?
I've tried taking the ItemsSource which was set to the ObservableCollection and converting it into an ObservableCollection< dynamic > but that didn't work.
I've tried Casting it as an ObservableCollection< T > and ObservableCollection< object > among others to no avail.
I've even tried restructuring my ViewModels under a GenericViewModel with a property of ObservableCollection< dynamic >, which failed and left my code in ruin so I had to return to a backup.
I've used an ItemsControl that changes the ListBox depending on which DataType it finds, but that would still mean I have to make separate button events anyway, so what's the point?
I would post some code, but seeing how nothing I've done has worked in the slightest I doubt that it will help any. At this point I don't even know if what I'm intending can be done at all.
If there are any suggestions on what code to post, feel free to ask.
EDIT: Here is a GenericViewModel. It doesn't work because I don't know what to set "Anything" to. EDIT: Added the UserControl
public class GenericViewModel : Observable
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
public void MoveUp()
{
if (Items == null) return;
Helper.MoveItemUp(Items, _items.IndexOf(_selectedItem));
}
public void MoveDown()
{
if (Items == null) return;
Helper.MoveItemDown(Items, _items.IndexOf(_selectedItem));
}
public void Remove()
{
if (Items == null) return;
Helper.RemoveItem(Items, _items.IndexOf(_selectedItem));
}
}
UserControl
public partial class CustomListBox : UserControl
{
//-Fields
//-Properties
//-Dependencies
//-Constructor
public CustomListBox()
{
InitializeComponent();
}
//-Methods
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDown_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonCopy_Click(object sender, RoutedEventArgs e)
{
}
private void ListboxButtonDelete_Click(object sender, RoutedEventArgs e)
{
}
private void BorderLayerThumbnail_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
}
private void BorderLayerThumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
}
}
<UserControl x:Class="BRRG_Scrubber.User_Controls.CustomListBox"
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:BRRG_Scrubber"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="150">
<Grid Grid.Row="0" Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Name}" Grid.Row="0" FontSize="10" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
<!--ItemsSource="{Binding Items}" SelectedItem="{Binding Current}"-->
<ListBox x:Name="listBoxPlus" Grid.Row="1" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" >
<ListBox.Resources>
<Style x:Key="{x:Type ScrollBar}" TargetType="{x:Type ScrollBar}">
<Setter Property="Stylus.IsFlicksEnabled" Value="True" />
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="14" />
<Setter Property="MinWidth" Value="14" />
</Trigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type local:Document}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Variable}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Layer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.30*" />
<ColumnDefinition Width="0.70*" />
</Grid.ColumnDefinitions>
<Border x:Name="borderLayerThumbnail" BorderBrush="#FF707070" BorderThickness="1" Width="50" Height="50" MouseRightButtonDown="BorderLayerThumbnail_MouseRightButtonDown" MouseLeftButtonDown="BorderLayerThumbnail_MouseLeftButtonDown" >
<Border.Background>
<ImageBrush ImageSource="/BRRG_Scrubber;component/Resources/Images/checkerboardtile.jpg" ViewportUnits="Absolute" Stretch="None" Viewport="0,0,12,12" TileMode="Tile"/>
</Border.Background>
<Image Grid.Column="0" Source="{Binding Image}" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" OpacityMask="Gray">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0,0,0">
<TextBox Text="{Binding Name}"/>
<TextBlock Text="{Binding Type, Mode=OneWay}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Text="👁" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Visible}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="🔒" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Opacity" Value="1.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Locked}" Value="False">
<Setter Property="Opacity" Value="0.2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
<WrapPanel Grid.Row="2" HorizontalAlignment="Right">
<WrapPanel.Resources>
<Style TargetType="Button">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
</Style>
</WrapPanel.Resources>
<Button x:Name="listboxButtonUp" Content="▲" Click="ListboxButtonUp_Click"/>
<Button x:Name="listboxButtonDown" Content="▼" Click="ListboxButtonDown_Click"/>
<Button x:Name="listboxButtonCopy" Content="⧉" Click="ListboxButtonCopy_Click"/>
<Button x:Name="listboxButtonDelete" Content="⛞" Click="ListboxButtonDelete_Click"/>
</WrapPanel>
</Grid>
</UserControl>
I would really like to be able to create a modified ListBox in a UserControl with buttons that can move items Up and Down and Remove them from the list which I can use for any ObservableCollection of any unknown Type. The ListBoxes I need would all function exactly the same except their Type would be unknown until runtime.
EDIT: New Code From Ed's Suggestions
MainViewModel
public class MainViewModel : Observable
{
//-Fields
private Project _project;
private GenericViewModel<Document> _documentCollection;
private GenericViewModel<Variable> _variableCollection;
private GenericViewModel<Layer> _layerCollection;
//-Properties
public Project Project
{
get { return _project; }
set { Set(ref _project, nameof(Project), value); }
}
public GenericViewModel<Document> DocumentCollection
{
get { return _documentCollection; }
set { Set(ref _documentCollection, nameof(DocumentCollection), value); OnPropertyChanged(nameof(LayerCollection)); }
}
public GenericViewModel<Variable> VariableCollection
{
get { return _variableCollection; }
set { Set(ref _variableCollection, nameof(VariableCollection), value); }
}
public GenericViewModel<Layer> LayerCollection
{
get { return _layerCollection; }
set { Set(ref _layerCollection, nameof(LayerCollection), value); }
}
//-Constructors
public MainViewModel()
{
Project = new Project();
DocumentCollection = new GenericViewModel<Document>();
DocumentCollection.Items = Project.Documents;
}
//-Logic
}
Test Window with Bindings
<StackPanel>
<uc:CustomListBox DataContext="{Binding DocumentCollection}" Height="100"/>
<uc:CustomListBox DataContext="{Binding LayerCollection}" Height="200"/>
<ListBox ItemsSource="{Binding Project.Documents}" Height="100"/>
</StackPanel>
GenericViewModel
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
{
//-Fields
private ObservableCollection<Anything> _items;
private Anything _selectedItem;
//-Properties
public ObservableCollection<Anything> Items
{
get { return _items; }
set { Set(ref _items, nameof(Items), value); }
}
public Anything SelectedItem
{
get { return _selectedItem; }
set { Set(ref _selectedItem, nameof(SelectedItem), value); }
}
//-Constructors
public GenericViewModel()
{
if (Items == null) Items = new ObservableCollection<Anything>();
}
//-Logic
...Removed For Brevity...
}
Document Model Class
public class Document : Anything
{
//-Fields
private string _filePath = "New Document";
private ObservableCollection<Layer> _layers;
private ObservableCollection<Selection> _selections;
//-Properties
public string FilePath
{
get { return _filePath; }
set { Set(ref _filePath, nameof(FilePath), value); }
}
public ObservableCollection<Layer> Layers
{
get { return _layers; }
set { Set(ref _layers, nameof(Layers), value); }
}
//-Constructors
public Document()
{
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
}
public Document(string filepath)
{
this.FilePath = filepath;
if (Layers == null) Layers = new ObservableCollection<Layer>();
if (Selections == null) Selections = new ObservableCollection<Selection>();
Layers.Add(new Layer("LayerOne "+Name));
Layers.Add(new Layer("LayerTwo " + Name));
Layers.Add(new Layer("LayerThree " + Name));
Selections.Add(new Selection());
Selections.Add(new Selection());
}
//-Gets
public string Name
{
get { return Path.GetFileNameWithoutExtension(FilePath); }
}
}
The big issue here seems to be that you can't cast to a generic class with an unknown type parameter, but you want your viewmodel class to be properly generic. That circle is squareable in two different useful ways, both valuable to know, so we'll do both.
The proper MVVM way to do this is to give your viewmodel some command properties which call these methods. A DelegateCommand class is the same as a RelayCommand class; the internet is full of implementations if you don't have one already.
public ICommand MoveUpCommand { get; } =
new DelegateCommand(() => MoveUp());
XAML:
<Button Content="▲" Command="{Binding MoveUpCommand}" />
Then get rid of those click event handlers. You don't need 'em. That very neatly solves your problem of calling those methods.
However, there's also a clean way to call those methods from code behind, and it's an important pattern to learn if you're going to be working with generics.
The classic solution to the casting problem is the one the framework uses for generic collections: Generic IEnumerable<T> implements non-generic System.Collections.IEnumerable. List<T> implements non-generic System.Collections.IList. Those non-generic interfaces provide the same operations, but in a non-generic way. You can always cast a List<T> to non-generic IList and call those methods and properties without knowing T.
Any well-designed collection can be assigned to a property of type IEnumerable: ListBox.ItemsSource is declared as System.Collections.IEnumerable, for example. Any collection can be assigned to it, without the ListBox needing to know what type is in the collection.
So let's write a non-generic interface that exposes the members we'll need to access without knowing any type parameters.
public interface ICollectionViewModel
{
void MoveUp();
void MoveDown();
void Remove();
}
If one of those method prototypes included the collection item type, say void RemoveItem(Anything x), that would complicate matters, but there's a classic solution to that problem as well.
Your Anything is already used like a type parameter. All we need to do is declare it as one. Your methods already have the appropriate prototypes to implement the interface methods.
public class GenericViewModel<Anything> : Observable, ICollectionViewModel
Instantiate like so:
this.DocumentCollection = new GenericViewModel<Document>();
Now your codebehind can cast any instance of GenericViewModel, regardless of type parameter, to a non-generic interface that supports the needed operations:
private void ListboxButtonUp_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ICollectionViewModel icollvm)
{
icollvm.MoveUp();
}
}
I have an observable collection that I am displaying in a Xamarin Forms ListView. I have defined a detail and a summary template that I use to view each list item. I want to be able to dynamically change between summary and detail template based on a Boolean property in each item.
Here is the item.
public class MyItem : INotifyPropertyChanged
{
bool _switch = false;
public bool Switch
{
get
{
return _switch;
}
set
{
if (_switch != value)
{
_switch = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Switch"));
}
}
}
public int Addend1 { get; set; }
public int Addend2 { get; set; }
public int Result
{
get
{
return Addend1 + Addend2;
}
}
public string Summary
{
get
{
return Addend1 + " + " + Addend2 + " = " + Result;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Here is the observable collection. Note that whenever the switch value changes I remove the item and reinsert. The reason this is done is to force the ListView to reselect the DataTemplate.
public class MyItems : ObservableCollection<MyItem>
{
protected override void InsertItem(int index, MyItem item)
{
item.PropertyChanged += MyItems_PropertyChanged;
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
this[index].PropertyChanged -= MyItems_PropertyChanged;
base.RemoveItem(index);
}
private void MyItems_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
int index = IndexOf(sender as MyItem);
if(index >= 0)
{
RemoveAt(index);
Insert(index, sender as MyItem);
}
}
}
Here is my data template selector...
public class MyItemTemplateSelector : DataTemplateSelector
{
DataTemplate Detail { get; set; }
DataTemplate Summary { get; set; }
protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
{
if(item is MyItem)
{
return (item as MyItem).Switch ? Detail : Summary;
}
return null;
}
}
Here are my resource definitions...
<DataTemplate x:Key="MyDetail">
<ViewCell>
<StackLayout Orientation="Horizontal">
<Switch IsToggled="{Binding Switch}"/>
<Entry Text="{Binding Addend1}"/>
<Entry Text="{Binding Addend2}"/>
<Label Text="{Binding Result}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
<DataTemplate x:Key="MySummary">
<ViewCell>
<StackLayout Orientation="Horizontal">
<Switch IsToggled="{Binding Switch}"/>
<Label Text="{Binding Summary}" VerticalOptions="Center"/>
</StackLayout>
</ViewCell>
</DataTemplate>
<local:MyItemTemplateSelector x:Key="MySelector" Detail="{StaticResource MyDetail}" Summary="{StaticResource MySummary}"/>
Here is my collection initialization...
MyItems = new MyItems();
MyItems.Add(new MyItem() { Switch = true, Addend1 = 1, Addend2 = 2 });
MyItems.Add(new MyItem() { Switch = false, Addend1 = 1, Addend2 = 2 });
MyItems.Add(new MyItem() { Switch = true, Addend1 = 2, Addend2 = 3 });
MyItems.Add(new MyItem() { Switch = false, Addend1 = 2, Addend2 = 3 });
And this is what it looks like...
Right. So everything works fine. If the switch is toggled the view of the item changes from summary to detail. The problem is that this cannot be the right way of doing this! It is a complete kluge to remove a list item and put it back in the same place in order to get the data template to reselect. But I cannot figure out another way of doing it. In WPF I used a data trigger in an item container style to set the content template based on the switch value, but there seems to be no way to do the equivalent thing in Xamarin.
The way to do this is not through switching templates, but defining a content view as the template and changing the visibility of controls within the template. There is apparently no way to get the ListView to re-evaluate the item template on an item short of removing it and re-adding it.
Here is my content view...
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamarinFormsBench"
x:Class="XamarinFormsBench.SummaryDetailView">
<ContentView.Content>
<StackLayout x:Name="stackLayout" Orientation="Horizontal">
<Switch x:Name="toggle" IsToggled="{Binding Switch}"/>
<Entry x:Name="addend1" Text="{Binding Addend1}"/>
<Entry x:Name="addend2" Text="{Binding Addend2}"/>
<Label x:Name="result" Text="{Binding Result}"/>
<Label x:Name="summary" Text="{Binding Summary}" VerticalOptions="Center"/>
</StackLayout>
</ContentView.Content>
This is the code behind...
namespace XamarinFormsBench
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SummaryDetailView : ContentView
{
public SummaryDetailView()
{
InitializeComponent();
toggle.PropertyChanged += Toggle_PropertyChanged;
UpdateVisibility();
}
private void Toggle_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if(e.PropertyName == "IsToggled")
{
UpdateVisibility();
}
}
private void UpdateVisibility()
{
bool isDetail = toggle.IsToggled;
addend1.IsVisible = isDetail;
addend2.IsVisible = isDetail;
result.IsVisible = isDetail;
summary.IsVisible = !isDetail;
InvalidateLayout(); // this is key!
}
}
}
Now the main page contains this...
<ListView ItemsSource="{Binding MyItems}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<local:SummaryDetailView/>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The key to making this work properly is to invalidate the layout of the ContentView when switching between summary and detail. This forces the ListView to layout the cell again. Without this the controls that are made invisible disappear the controls made visible never show. You do not need this if the ContentView is used outside of the ListView. This seems to me to be a bug in the ListView. You could get the item template switching to work if you could invalidate the layout of the ViewCell, but there is no public method (only a protected one) to do this.
This was tricky issue for me few years ago. I've came to MarkupExtensions and converters (IValueConverter). After heavy struggle with XAML extensions realm I've figured an obvious thing: it shouldn't be done like that.
For dynamic change of (m)any property(ies) of the component you should use Styles. Reactions of property (it has to be DependencyProperty to work with components) changes are simple to set via Stryle.Triggers and Setters.
<Style x:Key="imbXmlTreeView_itemstyle" TargetType="TreeViewItem">
<Setter Property="Margin" Value="-23,0,0,0" />
<Setter Property="Padding" Value="1" />
<Setter Property="Panel.Margin" Value="0"/>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
<Setter Property="Foreground" Value="{DynamicResource fade_darkGray}" />
</Trigger>
<Trigger Property="IsSelected" Value="False">
<Setter Property="Background" Value="{DynamicResource fade_lightGray}" />
<Setter Property="Foreground" Value="{DynamicResource fade_darkGray}" />
</Trigger>
</Style.Triggers>
</Style>
Consider above (just copied from my old project): DynamicResource can be your DataTemplate.
Here is more accurate example you might use:
<Style x:Key="executionFlowBorder" TargetType="ContentControl" >
<Setter Property="Margin" Value="5" />
<Setter Property="ContentTemplate" >
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Border Style="{DynamicResource executionBorder}" DataContext="{Binding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<CheckBox IsChecked="{Binding Path=isExecuting}" Content="" Grid.Column="0" VerticalAlignment="Center"/>
<Label Content="{Binding Path=displayName, Mode=OneWay}" FontSize="10" Grid.Column="1" FontStretch="Expanded" FontWeight="Black"/>
<Image Source="{Binding Path=iconSource, Mode=OneWay}" Width="16" Height="16" Grid.Column="2" HorizontalAlignment="Right" Margin="0,0,5,0"/>
</Grid>
</Border>
<Label Content="{Binding Path=displayComment, Mode=OneWay}" FontSize="9" HorizontalAlignment="Left"/>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
Where the value of setter can be DynamicResource or one delivered via your MarkupExtension - some thing like I had here:
using System;
using System.Windows;
using System.Windows.Markup;
#endregion
/// <summary>
/// Pristup glavnom registru resursa
/// </summary>
[MarkupExtensionReturnType(typeof (ResourceDictionary))]
public class masterResourceExtension : MarkupExtension
{
public masterResourceExtension()
{
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
try
{
return imbXamlResourceManager.current.masterResourceDictionary;
}
catch
{
return null;
}
}
}
The MarkupExtensions you are using as in example below:
In the XAML code:
<Image Grid.Row="1" Name="image_splash" Source="{imb:imbImageSource ImageName=splash}" Stretch="Fill" />
Added later: just don't forget to add namespace/assembly reference (pointing to the code with the custom MarkupExtension) at top of the XAML Window/Control (in this example it is imbCore.xaml from separate library project of the same solution):
<Window x:Class="imbAPI.imbDialogs.imbSplash"
xmlns:imb="clr-namespace:imbCore.xaml;assembly=imbCore"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding Path=splashTitle}" Height="666" Width="896" ResizeMode="NoResize" WindowStyle="ToolWindow" Topmost="False" WindowStartupLocation="CenterScreen"
xmlns:imbControls="clr-namespace:imbAPI.imbControls">
<Grid>
Also have in mind you have to compile it first in order to get it working in XAML designer.
The C# code of the extension used:
using System;
using System.Windows.Markup;
using System.Windows.Media;
using imbCore.resources;
#endregion
[MarkupExtensionReturnType(typeof (ImageSource))]
public class imbImageSourceExtension : MarkupExtension
{
public imbImageSourceExtension()
{
}
public imbImageSourceExtension(String imageName)
{
this.ImageName = imageName;
}
[ConstructorArgument("imageName")]
public String ImageName { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
try
{
if (imbCoreApplicationSettings.doDisableIconWorks) return null;
return imbIconWorks.getIconSource(ImageName);
}
catch
{
return null;
}
}
}
Hope I got your question right on the first place :).
Now I have to sleap :). Good luck!
Added later: ok, I missed your point :) sorry. However, I would leave the response in case you find something useful in the codes I've posted. Bye!
This is probably a newbie question but I’m breaking my head about this one.
I’m building an application in WPF using the MVVM pattern. I have a view which has fields binded a property exposing the current customer entity. And within the view I have a command to change the bank account numbers belonging to that customer. The command called take the whole sub-entity as parameter. Then call another function which also takes the sub-entity as parameter and passes it to a new viewmodel binded to a new view which is displayd as a dialog for change. This all works. But when I’m change the bank account number in the dialog the original view also changes the account number number real-time. They are still connected to each other. I want to cancel this link to be able to cancel the dialog and the changes I made within that dialog. But I can’t get this to work.
Code say’s more the words.
View MAIN
<dxlc:LayoutGroup Header="Rekeningen" View="GroupBox" Orientation="Vertical" VerticalAlignment="Stretch">
<dxlc:LayoutItem>
<StackPanel>
<Button Content="{x:Static language:Common.NieuwRekeningnrToevoegen}" Command="{Binding NieuwRekeningCommand}" />
<ListView ItemsSource="{Binding CurrentRelatie.Rekeningnummers}" ItemTemplate="{StaticResource RelatieRekeningnrTemplate}" />
</StackPanel>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
View item template MAIN
<DataTemplate x:Key="RelatieRekeningnrTemplate">
<Grid>
<TextBlock >
<Run Text="{Binding Omschrijving}" FontWeight="Bold" FontStyle="Italic" /> <LineBreak/>
<Run Text="{Binding RekNummer}" /> - <Run Text="{Binding BicNummer}" FontStyle="Italic" />
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
<Button Command="{Binding DataContext.VerwijderRekeningCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" CommandParameter="{Binding}">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="BorderBrush" Value="{x:Null}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template" Value="{DynamicResource tplFlatButton}" />
</Style>
</Button.Style>
<Path Height="9" Stretch="Uniform" Fill="{DynamicResource AccentColorDarkGray}" Data="{DynamicResource Delete}" />
</Button>
<Button Command="{Binding DataContext.EditRekeningCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" CommandParameter="{Binding}">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{x:Null}" />
<Setter Property="BorderBrush" Value="{x:Null}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Template" Value="{DynamicResource tplFlatButton}" />
</Style>
</Button.Style>
<Path Height="10" Stretch="Uniform" Fill="{DynamicResource AccentColorDarkGray}" Data="{DynamicResource Edit}" >
</Path>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
Viewmodel
private model.Relatie _CurrentRelatie = null;
public model.Relatie CurrentRelatie
{
get { return _CurrentRelatie; }
set { SetProperty(ref _CurrentRelatie, value, () => CurrentRelatie); }
}
public ICommand EditRekeningCommand { get; private set; }
void OnEditRekeningExecute(model.Rekeningnummer Rek)
{
EditRekeningnummer(Rek);
}
private void EditRekeningnummer(model.Rekeningnummer Rek)
{
Dialog.dRekeningnummerEditViewModel ReknummerVM = new Dialog.dRekeningnummerEditViewModel();
ReknummerVM.SetRekening(Rek);
UICommand ResCommand = DialogService.ShowDialog(ReknummerVM.DialogUICommand, string.Format("{0} {1}", Common.Rekening, Rek.Omschrijving ?? Rek.RekNummer), "viewdRekeningnummerEdit", ReknummerVM);
if (ResCommand == null || ResCommand.IsCancel == true)
return;
}
View RekeningnummerEdit
<dxlc:LayoutGroup Orientation="Vertical">
<dxlc:LayoutItem Label="{Binding CurrentRekening, ConverterParameter=Omschrijving, Converter={StaticResource ModelToDisplay}}">
<dxe:TextEdit EditValue="{Binding CurrentRekening.Omschrijving, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" ValidateOnTextInput="True" utils:FocusAdvancement.AdvancesByEnterKey="true"/>
</dxlc:LayoutItem>
<dxlc:LayoutItem Label="{Binding CurrentRekening, ConverterParameter=RekNummer, Converter={StaticResource ModelToDisplay}}">
<dxe:TextEdit EditValue="{Binding CurrentRekening.RekNummer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" ValidateOnTextInput="True" utils:FocusAdvancement.AdvancesByEnterKey="true"/>
</dxlc:LayoutItem>
<dxlc:LayoutItem Label="{Binding CurrentRekening, ConverterParameter=BicNummer, Converter={StaticResource ModelToDisplay}}">
<dxe:TextEdit EditValue="{Binding CurrentRekening.BicNummer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" ValidateOnTextInput="True" utils:FocusAdvancement.AdvancesByEnterKey="true"/>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
Viewmodel RekeningnummerEdit
public dRekeningnummerEditViewModel()
{
DialogUICommand = new List<UICommand>();
AnnuleerUICommand = new UICommand() {
Caption=Common.Annuleren,
Id = MessageBoxResult.Cancel,
IsCancel=true
};
OKUICommand = new UICommand() {
Caption=Common.Opslaan,
Id = MessageBoxResult.OK,
IsDefault=true
};
DialogUICommand.Add(OKUICommand);
DialogUICommand.Add(AnnuleerUICommand);
CurrentRekening = new model.Rekeningnummer();
}
public void SetRekening(model.Rekeningnummer Rek)
{
CurrentRekening = Rek;
IsInEditMode = true;
}
#region "Properties"
private model.Rekeningnummer _CurrentRekening;
public model.Rekeningnummer CurrentRekening
{
get { return _CurrentRekening; }
set { SetProperty(ref _CurrentRekening, value, () => CurrentRekening); }
}
#endregion
#region "Private function"
#endregion
#region "Commands"
public List<UICommand> DialogUICommand { get; private set; }
protected UICommand AnnuleerUICommand { get; private set; }
protected UICommand OKUICommand { get; private set; }
The behaviour you're seeing is because you are passing an object reference (model.Rek) from your view to your dialog. Therefore when your dialog changes the values of the model.Rek, the changes are immediately reflected in your view.
A common approach to solve this problem is to:
Clone (copy) your model, i.e. make a new object with the same values. You can use the ICloneable interface as a standard pattern (MemberwiseClone can help here if you only need a shallow copy)
Send the clone to your dialog
If the user presses OK, then take the values of the clone and copy them back to the original model. If the user presses Cancel, then do nothing more
So I have a listbox and a tool bar in my WPF app. The tool bar just has regular controls, and the listbox has vertical expanders.
I need the listbox to have a different set of expanders depending on what button is clicked. Right now it looks like such:
<ListBox>
<local:Select_Analysis_Panel/>
</ListBox>
Where local:Select_Analysis_Panel is seperate user control file containing the expanders. What is the best way to go about dynamically updating the ListBox control's content upon a button click?
For the last couple hours I've been trying to use set DataTemplates for each expander set and bind the to the items control property with little avail with the code below. I'm just trying to get basic framework laid out before setting up a MVVM interface. Later on I was going to replace the ItemsSource="Network_anal" with you know ItemsSource="{Binding WhatExpanderViewModelProperty}" or something like that.
<ListBox Width="250" Margin="5,0,0,0">
<ListBox.Resources>
<DataTemplate DataType="Select_Analysis_Panel">
<local:Select_Analysis_Panel/>
</DataTemplate>
<DataTemplate x:Key="Network_anal" DataType="NetworkAnalysis">
<local:NetworkAnalysis/>
</DataTemplate>.Resources>
<ListBox.Template>
<ControlTemplate>
<Border Background="Red"/>
</ControlTemplate>
</ListBox.Template>
<ItemsControl ItemsSource="Network_anal"/>
</ListBox>
Am I taking the right approach to this at all?
Here's what I'm trying to do. Below when the "File" button is clicked the side bar displays these 2 expanders:
And when "Network Design" button these expanders are dipslayed:
Option 1:
Subclassing the sections:
each of these sections could be subclassed from a base section class and a specific DataTemplate could be used for each:
<Window x:Class="MiscSamples.MultiToolbar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
Title="MultiToolbar" Height="300" Width="300">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<DockPanel>
<ListBox ItemsSource="{Binding Sections}"
SelectedItem="{Binding SelectedSection}"
DisplayMemberPath="Name"
DockPanel.Dock="Top">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Setter Property="MinWidth" Value="80"/>
<Setter Property="MinHeight" Value="40"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border BorderBrush="Black" BorderThickness="1">
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}">
<ContentPresenter ContentSource="Content"/>
</ToggleButton>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<ScrollViewer Width="300" DockPanel.Dock="Left">
<ContentPresenter Content="{Binding SelectedSection}">
<ContentPresenter.Resources>
<DataTemplate DataType="{x:Type local:FileSection}">
<TextBlock Text="User Control For File Section"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:NetworkDesignSection}">
<TextBlock Text="User Control For Network Design"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SelectAnalysisSection}">
<TextBlock Text="User Control For Select Analysis"/>
</DataTemplate>
</ContentPresenter.Resources>
</ContentPresenter>
</ScrollViewer>
<Grid Background="Gray">
<TextBlock Text="Design Surface" TextAlignment="Center" VerticalAlignment="Center" FontWeight="Bold"/>
</Grid>
</DockPanel>
</Window>
Code Behind:
public partial class MultiToolbar : Window
{
public MultiToolbar()
{
InitializeComponent();
var vm = new MainViewModel();
vm.Sections.Add(new FileSection() {Name = "File"});
vm.Sections.Add(new NetworkDesignSection() { Name = "Network Design" });
vm.Sections.Add(new SelectAnalysisSection() { Name = "Select Analysis" });
DataContext = vm;
}
}
Main ViewModel:
public class MainViewModel: PropertyChangedBase
{
private ObservableCollection<Section> _sections;
public ObservableCollection<Section> Sections
{
get { return _sections ?? (_sections = new ObservableCollection<Section>()); }
}
private Section _selectedSection;
public Section SelectedSection
{
get { return _selectedSection; }
set
{
_selectedSection = value;
OnPropertyChanged("SelectedSection");
}
}
}
Sections:
public abstract class Section:PropertyChangedBase
{
public string Name { get; set; }
private bool _isEnabled = true;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged("IsEnabled");
}
}
private bool _isVisible = true;
public bool IsVisible
{
get { return _isVisible; }
set
{
_isVisible = value;
OnPropertyChanged("IsVisible");
}
}
//Optionally
//public string ImageSource {get;set;}
//ImageSource = "/Resources/MySection.png";
}
public class FileSection: Section
{
///... Custom logic specific to this Section
}
public class NetworkDesignSection:Section
{
///... Custom logic specific to this Section
}
public class SelectAnalysisSection: Section
{
///... Custom logic specific to File Section
}
//...etc etc etc
Result:
Notice that I'm using ToggleButtons bound to the ListBoxItem.IsSelected property to simulate a TabControl-like behavior.
You can set the DataContext of the whole form and bind the ItemsSource of the listbox, or set ItemsSource of the listbox to some collection directly.