I have a tree view built with HierarchicalDataTemplates, I want to be able to add JSON files to SegmentInfo nodes - if I do so, the data is added but the change is not reflected in UI (still the comment says "no data" in red).
I've made the list the tree view items as ObservableCollection, moved it to a "ViewModel" class that inherits INotifyPropertyChanged, I seem to set it up properly, I've set DataContext to the ViewModel object in my Window.
In xaml I've set the bindings and mode as TwoWay. Still nothing helped
XAML:
<Window.Resources>
<local:BoolToStringConverter x:Key="BoolToStringConverter" FalseValue="no data" TrueValue="has data" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" MinHeight="384.8"/>
<RowDefinition Height="35.2"/>
</Grid.RowDefinitions>
<TreeView Name="trvTypeInfos" Margin="5" Grid.Row="0" ItemsSource="{Binding Path=TypeInfoList, Mode=TwoWay}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="ListBoxItem.PreviewMouseUp"
Handler="ListBoxItem_PreviewMouseUp"/>
<Setter Property="IsExpanded" Value="True"/>
</Style>
<HierarchicalDataTemplate DataType="{x:Type data:TypeInfo}" ItemsSource="{Binding SegmentInfos, Mode=TwoWay}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=" [" Foreground="Blue" />
<TextBlock Text="{Binding SegmentInfos.Count}" Foreground="Blue"/>
<TextBlock Text="]" Foreground="Blue" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type data:SegmentInfo}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=" ("/>
<TextBlock Text="{Binding Path=HasData, Mode=TwoWay, Converter={StaticResource BoolToStringConverter}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="no data">
<Setter Property="Foreground" Value="Red"/>
</Trigger>
<Trigger Property="Text" Value="has data">
<Setter Property="Foreground" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text=")"/>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="80" Height="20" Content="OK" Margin="5,0, 5, 5" IsDefault="True" Click="OK_Click"/>
<Button Width="80" Height="20" Content="Cancel" Margin="5,0, 5, 5" Click="Cancel_Click" />
</StackPanel>
</Grid>
Window class:
public SegmentDataUpdaterDialog(SegmentDataUpdater segmentDataUpdater, List<TypeInfo> typeInfoList)
{
ViewModel = new ViewModel(typeInfoList);
DataContext = ViewModel;
SegmentDataUpdater = segmentDataUpdater;
InitializeComponent();
}
private void ListBoxItem_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
SegmentInfo segInfo = item.Header as SegmentInfo;
if (segInfo != null)
{
MessageBox.Show(segInfo.JsonContents);
var filePath = AskForFile();
bool success = SegmentDataUpdater.TryStoreJson(segInfo, filePath, out string json);
if (success)
{
segInfo.JsonContents = json;
segInfo.HasData = true;
}
}
}
ViewModel class:
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<TypeInfo> _typeInfoList;
public ObservableCollection<TypeInfo> TypeInfoList
{
get { return _typeInfoList; }
set
{
if (_typeInfoList==null || !value.All(_typeInfoList.Contains))
{
_typeInfoList = value;
OnPropertyChanged(nameof(TypeInfoList));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel(List<TypeInfo> typeInfos)
{
TypeInfoList = new ObservableCollection<TypeInfo>(typeInfos);
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
TypeInfo class:
public class TypeInfo
{
public string Name { get; set; }
public ObservableCollection<SegmentInfo> SegmentInfos { get; set; }
public int ElementId { get; set; }
public TypeInfo()
{
SegmentInfos = new ObservableCollection<SegmentInfo>();
}
}
SegmentInfo class:
public class SegmentInfo
{
public string Name { get; set; }
public bool HasData { get; set; }
public string JsonContents { get; set; }
public int ElementId { get; set; }
}
Converter classes:
public class BoolToValueConverter<T> : IValueConverter
{
public T FalseValue { get; set; }
public T TrueValue { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null)
return FalseValue;
else
return (bool)value ? TrueValue : FalseValue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value != null ? value.Equals(TrueValue) : false;
}
}
public class BoolToStringConverter : BoolToValueConverter<String> { }
I expect that after successful adding json file to the SegmentInfo the UI will update the node with "has data" comment.
Now I can check that the data is really added to the SegmentInfo but UI doesn't reflect that.
Your HasData property does not update the UI, as you have no mechanism to update it (INotifyPropertyChanged). SegmentInfo needs to implement INotifyPropertyChanged.
If you plan to have a property Bind to the UI, it needs to have an PropertyChanged Notification go out for it individually. So on your SegmentInfo class; Name, HasData, and JsonContent should each raise an OnPropertyChanged event in their setter.
A good way to think of it; anything that is directly bound in XAML (Text="{Binding Name}") should raise an event when changed. If you bind any properties like: (Text="{Binding MyThing.Name}") you will not get an update when MyThing.Name changes. You need to break out the property and Notify on it directly.
Related
I want to mark a ListViewItem from a ListView by changing the foreground color of the textblock which is in the listviewitem. I want to do that dynamically. Whenever a user selects the item, the TextBlock's foreground color should be changed. Here is the Templete
<ListView Name="SongsListView"
IsItemClickEnabled="True"
ItemClick="SongsListView_ItemClick"
ItemsSource="{x:Bind rootpage.Songs}"
VerticalAlignment="Top"
HorizontalAlignment="Stretch">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Song">
<controls:DropShadowPanel ShadowOpacity="0.20"
Color="Black"
HorizontalContentAlignment="Stretch"
BlurRadius="10"
OffsetX="0"
OffsetY="7.0">
<Grid HorizontalAlignment="Stretch" CornerRadius="5" Background="{ThemeResource SystemControlAltHighAcrylicElementBrush}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name}" Name="songNameTextBlock" TextWrapping="Wrap"/>
<TextBlock Text="{x:Bind Artist}" Name="ArtistNameTextBlock" TextWrapping="Wrap"/>
</StackPanel>
</Grid>
</controls:DropShadowPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
private void SongsListView_ItemClick(object sender, ItemClickEventArgs e)
{
//Please write the code for me !
}
I would suggest you to define a foreground relevant property in your Song class and bind it to the TextBlock's Foreground property. Then you could use Binding/{x:Bind} to change its foreground color when it's selected, instead of using the ItemClick event handler method to find the TextBlock controls in it.
Please refer to the following code sample for details:
<ListView Name="SongsListView"
IsItemClickEnabled="True"
ItemsSource="{x:Bind Songs}"
VerticalAlignment="Top"
HorizontalAlignment="Stretch" SelectedItem="{x:Bind currentSelectedItem,Mode=TwoWay,Converter={StaticResource myconverter}}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Song">
<controls:DropShadowPanel ShadowOpacity="0.20"
Color="Black"
HorizontalContentAlignment="Stretch"
BlurRadius="10"
OffsetX="0"
OffsetY="7.0">
<Grid HorizontalAlignment="Stretch" CornerRadius="5" Background="{ThemeResource SystemControlAltHighAcrylicElementBrush}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind Name}" Name="songNameTextBlock" TextWrapping="Wrap" Foreground="{x:Bind customcolor,Mode=OneWay}"/>
<TextBlock Text="{x:Bind Artist}" Name="ArtistNameTextBlock" TextWrapping="Wrap" Foreground="{x:Bind customcolor,Mode=OneWay}"/>
</StackPanel>
</Grid>
</controls:DropShadowPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Margin" Value="4"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
<Page.Resources>
<local:MyConverter x:Key="myconverter"></local:MyConverter>
</Page.Resources>
public sealed partial class MainPage : Page
{
public ObservableCollection<Song> Songs { get; set; }
private Song _currentSelectedItem;
public Song currentSelectedItem
{
get { return _currentSelectedItem; }
set
{
if (_currentSelectedItem != null)
{
_currentSelectedItem.customcolor = new SolidColorBrush(Colors.Black);
}
_currentSelectedItem = value;
_currentSelectedItem.customcolor = new SolidColorBrush(Colors.Red);
}
}
public MainPage()
{
this.InitializeComponent();
Songs = new ObservableCollection<Song>();
Songs.Add(new Song() {Name="abc",Artist="Singer1" });
Songs.Add(new Song {Name="def",Artist="Singer2" });
}
}
public class Song : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
private string _Name;
public string Name
{
get { return _Name; }
set
{
_Name = value;
RaisePropertyChanged("Name");
}
}
private string _Artist;
public string Artist
{
get { return _Artist; }
set
{
_Artist = value;
RaisePropertyChanged("Artist");
}
}
private SolidColorBrush _customcolor = new SolidColorBrush(Colors.Black);
public SolidColorBrush customcolor
{
get { return _customcolor; }
set
{
_customcolor = value;
RaisePropertyChanged("customcolor");
}
}
}
public class MyConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value as Song;
}
}
Please note that I make a converter class and use it when I bind currentSelectedItem to ListView's SelectedItem. Because I used x:Bind, it's compile time. If you do not add a converter, you will get error Invalid binding path 'currentSelectedItem' : Cannot bind type 'AppListView.model.Song' to 'System.Object' without a converter.
Besides, you could see I set Mode=OneWay when I use {x:Bind} to bind customcolor to TextBlock's foreground. It's due to the default value of {x:Bind}'s mode is OneTime. If you do not change OneTime to OneWay, you will not see the foreground color changed.
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!
I am currently following this guide for setting up a TreeView with checkboxes. In my code, the tree "FooViewModel" is initiated in my MainViewModel and bound to the TreeView as an ItemsSource. I want to be able to subscribe to some event in the MainViewModel that will trigger when something is checked or unchecked. That way I can iterate through the "FooViewModel" and check which nodes have IsChecked = True. How do I create this event binding?
This is the code I have:
<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False" />
<Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="xn:VirtualToggleButton.IsVirtualToggleButton" Value="True" />
<Setter Property="xn:VirtualToggleButton.IsChecked" Value="{Binding IsChecked}" />
<Setter Property="Focusable" Value="False" />
</Style>
<xn:TreeView ItemsSource="{Binding CollectionFooViewModel}" ItemContainerStyle="{StaticResource TreeViewItemStyle}">
<xn:TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center"/>
<ContentPresenter Content="{Binding Name, Mode=OneTime}" Margin="2,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</xn:TreeView.ItemTemplate>
</xn:TreeView>
I figured if there's a way to bind "IsChecked" to two properties (one in FooViewModel, another in MainViewModel) I would have my answer.
Lots of ways to achieve this. One would be some kind of a pub/sub (messaging) implementation or maybe just bunch of Action delegates? Something like...
MainWindow
<Window x:Class="WpfApplication1.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="300"
Width="250">
<TreeView ItemsSource="{Binding CollectionFooViewModel}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False"
IsChecked="{Binding IsChecked}"
VerticalAlignment="Center"/>
<ContentPresenter Content="{Binding Name, Mode=OneTime}"
Margin="2,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Window>
DataContext
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Action<MyItem> action = item => Console.WriteLine(#"MyItem was clicked");
CollectionFooViewModel = new ObservableCollection<MyItem>()
{
new MyItem()
{
Name = "MyItem1",
Children = new List<MyItem>()
{
new MyItem()
{
Name = "MySubItem1",
IsChecked = false,
Action = item => Console.WriteLine(#"{0} invoked action", item.Name)
},
new MyItem()
{
Name = "MySubItem2",
IsChecked = true,
Action = item => Console.WriteLine(#"{0} state is {1} ", item.Name, item.IsChecked)
},
},
Action = action
}
};
}
public ObservableCollection<MyItem> CollectionFooViewModel { get; set; }
}
public class MyItem : ViewModelBase
{
private bool _isChecked;
public string Name { get; set; }
public bool IsChecked
{
get { return _isChecked; }
set
{
_isChecked = value;
if (Action != null)
Action.BeginInvoke(this, null, null);
}
}
public IEnumerable<MyItem> Children { get; set; }
public Action<MyItem> Action { get; set; }
}
Which gives you the following...
...and spits this out to console when clicked in order.
MyItem was clicked
MySubItem1 invoked action
MySubItem2 state is False
Of course, in your case, you might want to pass concrete method to delegate.
Try adding "OnPropertyEventChanged" method call in the setter for your model, here is an example of what I mean:
https://msdn.microsoft.com/en-us/library/ms743695%28v=vs.110%29.aspx
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();
}
}
If I have this structure:
public class Parent
{
public string Name{get; set;}
public List<Child> Childs {get; set;}
}
public class Child
{
public string Name{get; set;}
public string Value{get; set;}
public enum ValueType {get; set;}
}
public enum ValueType
{
Int,
Boolean,
String
};
public class ParentFactory
{
public List<Parent> Parents {get; set;}
public ParentFactory()
{
Child child1 = new Child() {Name="Filter1", Value="1", ValueType=ValueType.String};
Child child2 = new Child() {Name="isExecuted", Value="true", ValueType=ValueType.Boolean};
Child child3 = new Child() {Name="Width", Value="10", ValueType=ValueType.Int};
Parent parent1 = new Parent(){Name="Adam", Childs = new List<Child>(){child1, child2}};
Parent parent2 = new Parent(){Name="Kevin", Childs = new List<Child>(){child3}};
Parents = new List<Parent>(){parent1, parent2};
}
}
I want to bind the object: ParentFactory parentFactory = new ParentFactory() to ItemsControl:
<DockPanel>
<ItemsControl ItemsSource="{Binding Parents}">
</ItemsControl>
</DockPanel>
<Window.Resources>
<DataTemplate DataType="{x:Type Parent}">
<StackPanel Margin="2,2,2,1">
<Expander Header="{Binding Name}">
<ItemsControl ItemsSource="{Binding Childs}" />
</Expander>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type Child}">
<StackPanel>
<TextBox Grid.Column="0" Text="{Binding Name}" />
<TextBox Grid.Column="1" Text="{Binding Value}"/>
<TextBox Grid.Column="2" Text="{Binding ValueType}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
In the Stackpanel, there are one control: TextBox. However, I want it to be more dynamic: if the ValueType is a Boolean then use a Checkbox and else use a Textbox for the: <TextBox Grid.Column="1" Text="{Binding Value}"/>.
Is this possible, and if so, how can I achieve it?
You can change the DataTemplate dynamically
Xaml
<DataTemplate>
<DataTemplate.Resources>
<DataTemplate x:Key="Condition1">
<TextBox></TextBox> // Do you binding
</DataTemplate>
<DataTemplate x:Key="Condition2">
<CheckBox></CheckBox> // Do you binding
</DataTemplate>
</DataTemplate.Resources>
</DataTemplate>
<ContentPresenter x:Name="ContentField"
Content="{Binding}"
ContentTemplate="{StaticResource ResourceKey=Condition1}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=ValueType}" Value="1">
<Setter TargetName="ContentField" Property="ContentTemplate" Value="{StaticResource ResourceKey=Condition2}" />
</DataTrigger>
</DataTemplate.Triggers>
Be sure to set the Bindings correctly ... and make the DataTemplates for Condition1 and Condition2
hope it helps :)
I think the easiest way to do it is to have both of them in your panel and define two new boolean properties that will control their Visibility through a Boolean To Visibility ValueConverter.
public bool IsValueTypeBoolean
{
get
{
...Conditions that will return true for CheckBox.
}
}
public bool IsValueTypeOther
{
get
{
return !this.IsValueBoolean;
}
}
<TextBox Grid.Column="2" Visibility="{Binding IsValueTypeOther, Converter={StaticResource visibilityConverter}}"/>
<CheckBox Grid.Column="2" Visibility="{Binding IsValueTypeBoolean, Converter={StaticResource visibilityConverter}}"/>
Boolean To VisibilityConverter
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool myValue = (bool)value;
if (myValue)
return Visibility.Visible;
else
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Add this as a resource to your XAML:
<local:BooleanToVisibilityConverter x:Key="visibilityConverter"></local:VisibilityConverter>
I hope there are no typos...