How to create context-aware ListBoxItem template? - c#

I want to create XAML chat interface that will display messages differently depending on it's neighbours. Here's an example:
I think ListBox control is most suitable for this. I'm also thinking about different controls such as FlowDocumentReader but I've never used them. Also I need to mention that message's text should be selectable (across multiple messages) and I don't know how to achieve this with ListBox.
Update: The main point there is that if one side (viking in this case) send some messages in a row, the interface should concatenate those (use slim message header instead of full one). So, the look of message with header depends on whether previous message was sent by the same person.

If you were just interested in the formatting of the Headers (full or small) then a ListBox/ListView/ItemsControl with PreviousData in the RelativeSource binding is the way to go (as pointed out by anivas).
But since you added that you wanted to support for selection across multiple messages then this pretty much rules out ItemsControl and the classes that derives from it as far as I know. You'll have to use something like a FlowDocument instead.
Unfortunately FlowDocument doesn't have the ItemsSource property. There are examples of workarounds for this, like Create Flexible UIs With Flow Documents And Data Binding but this implementation pretty much makes my VS2010 crash (I didn't investigate the reason for this, might be an easy fix).
Here is how I would do it
First you design the Blocks of the FlowDocument in the designer and when you're satisfied you move them to a resource where you set x:Shared="False". This will enable you to create multiple instances of the resource instead of using the same one over and over. Then you use an ObservableCollection as the "source" for the FlowDocument and subscribe to the CollectionChanged event, and in the eventhandler you get a new instance of the resource, check if you want the full or small header, and then add the blocks to the FlowDocument. You could also add logic for Remove etc.
Example implementation
<!-- xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" -->
<Window.Resources>
<Collections:ArrayList x:Key="blocksTemplate" x:Shared="False">
<!-- Full Header -->
<Paragraph Name="fullHeader" Margin="5" BorderBrush="LightGray" BorderThickness="1" TextAlignment="Right">
<Figure HorizontalAnchor="ColumnLeft" BaselineAlignment="Center" Padding="0" Margin="0">
<Paragraph>
<Run Text="{Binding Sender}"/>
</Paragraph>
</Figure>
<Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>
</Paragraph>
<!-- Small Header -->
<Paragraph Name="smallHeader" Margin="5" TextAlignment="Right">
<Run Text="{Binding TimeSent, StringFormat={}{0:HH:mm:ss}}"/>
</Paragraph>
<!-- Message -->
<Paragraph Margin="5">
<Run Text="{Binding Message}"/>
</Paragraph>
</Collections:ArrayList>
</Window.Resources>
<Grid>
<FlowDocumentScrollViewer>
<FlowDocument Name="flowDocument"
FontSize="14" FontFamily="Georgia"/>
</FlowDocumentScrollViewer>
</Grid>
And the code behind could be along the following lines
public ObservableCollection<ChatMessage> ChatMessages
{
get;
set;
}
public MainWindow()
{
InitializeComponent();
ChatMessages = new ObservableCollection<ChatMessage>();
ChatMessages.CollectionChanged += ChatMessages_CollectionChanged;
}
void ChatMessages_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ArrayList itemTemplate = flowDocument.TryFindResource("blocksTemplate") as ArrayList;
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (ChatMessage chatMessage in e.NewItems)
{
foreach (Block block in itemTemplate)
{
bool addBlock = true;
int index = ChatMessages.IndexOf(chatMessage);
if (block.Name == "fullHeader" &&
(index > 0 && ChatMessages[index].Sender == ChatMessages[index - 1].Sender))
{
addBlock = false;
}
else if (block.Name == "smallHeader" &&
(index == 0 || ChatMessages[index].Sender != ChatMessages[index - 1].Sender))
{
addBlock = false;
}
if (addBlock == true)
{
block.DataContext = chatMessage;
flowDocument.Blocks.Add(block);
}
}
}
}
}
And in my sample, ChatMessage is just
public class ChatMessage
{
public string Sender
{
get;
set;
}
public string Message
{
get;
set;
}
public DateTime TimeSent
{
get;
set;
}
}
This will enable you to select text however you like in the messages
If you're using MVVM you can create an attached behavior instead of the code behind, I made a sample implementation of a similar scenario here: Binding a list in a FlowDocument to List<MyClass>?
Also, the MSDN page for FlowDocument is very helpful: http://msdn.microsoft.com/en-us/library/aa970909.aspx

Assuming your ItemTemplate is a StackPanel of TextBlock header and TextBlock message you can use a MultiBinding Visibility Converter to hide the header as:
<TextBlock Text="{Binding UserName}">
<TextBlock.Visibility>
<MultiBinding Converter="{StaticResource headerVisibilityConverter}">
<Binding RelativeSource="{RelativeSource PreviousData}"/>
<Binding/>
</MultiBinding>
</TextBlock.Visibility>
</TextBlock>
And the IMultiValueConverter logic goes something like:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var previousMessage = values[0] as MessageItem;
var currentMessage = values[1] as MessageItem;
if ((previousMessage != null) && (currentMessage != null))
{
return previousMessage.UserName.Equals(currentMessage.UserName) ? Visibility.Hidden : Visibility.Visible;
}
return Visibility.Visible;
}

Try to give a hint pseudocode like:
public abstract class Message {/*Implementation*/
public enum MessageTypeEnum {Client, Viking, None};
public abstract MessageTypeEnum MessageType {get;}
}
public class ClientMessage : Message {
/*Client message concrete implementation.*/
public override MessageTypeEnum MessageType
{
get {
return MessageTypeEnum.Client;
}
}
}
public class VikingMessage : Message
{
/ *Viking message concrete implementation*/
public override MessageTypeEnum MessageType
{
get {
return MessageTypeEnum.Viking;
}
}
}
After this in yor bindind code in XAML on binding control use XAML attribute Converter
Where you can assign a class reference which implements IValueConverter. Here are the links
Resource on web:
Converter
There you can converts the type between your UI/ModelView.
Hope this helps.

I don't think you can do this purely through XAML, you're going to need to have code written somewhere to determine the relationship between each message, i.e., is the the author of message n - 1 the same as n?
I wrote a very quick example that resulted in the desired output. My example and the resulting code snippets are in no way production level code, but it should at least point you in the right direction.
To start, I first created a very simple object to represent the messages:
public class ChatMessage
{
public String Username { get; set; }
public String Message { get; set; }
public DateTime TimeStamp { get; set; }
public Boolean IsConcatenated { get; set; }
}
Next I derived a collection from ObservableCollection to handle determining relationships between each message as they're added:
public class ChatMessageCollection : ObservableCollection<ChatMessage>
{
protected override void InsertItem(int index, ChatMessage item)
{
if (index > 0)
item.IsConcatenated = (this[index - 1].Username == item.Username);
base.InsertItem(index, item);
}
}
This collection can now be exposed by your ViewModel and bound to the ListBox in your view.
There are many ways to display templated items in XAML. Based on your example interface, the only aspect of each item changing is the header so I figured it made the most sent to have each ListBoxItem display a HeaderedContentControl that would show the correct header based on the IsConcatenated value:
<ListBox ItemsSource="{Binding Path=Messages}" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type m:ChatMessage}">
<HeaderedContentControl Header="{Binding}">
<HeaderedContentControl.HeaderTemplateSelector>
<m:ChatHeaderTemplateSelector />
</HeaderedContentControl.HeaderTemplateSelector>
<Label Content="{Binding Path=Message}" />
</HeaderedContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You'll notice that I am specifying a HeaderTemplateSelector which is responsible for choosing between one of two header templates:
public sealed class ChatHeaderTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var chatItem = item as ChatMessage;
if (chatItem.IsConcatenated)
return ((FrameworkElement)container).FindResource("CompactHeader") as DataTemplate;
return ((FrameworkElement)container).FindResource("FullHeader") as DataTemplate;
}
}
And finally, here are the two header templates which are defined as resources of the view:
<DataTemplate x:Key="FullHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="{Binding Path=Username}" />
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="CompactHeader">
<Border
Background="Lavender"
BorderBrush="Purple"
BorderThickness="1"
CornerRadius="4"
HorizontalAlignment="Right"
Padding="2"
>
<DockPanel>
<TextBlock DockPanel.Dock="Right" HorizontalAlignment="Right" Text="{Binding Path=TimeStamp, StringFormat='{}{0:HH:mm:ss}'}" />
</DockPanel>
</Border>
</DataTemplate>
Again, this example is not perfect and is probably just one of many that works, but at least it should point you in the right direction.

Related

WPF Complex Logic of Custom Controls with MVVM

I am creating a WPF-based plugin (for Revit, an architectural 3D modelling software, but this shouldn't matter) which is quite complex and I'm getting kind of lost.
The WPF Window is composed by 2 tabs and each Tab is a custom UserControl that I'm inserting in the TabItem through a Frame. The Main Window has a ViewModel where the data is bound.
One of the tabs helps with the creation of floors in a 3D model
part of MainWindow.xaml
<TabItem Name="LevelsTab" Header="Levels" HorizontalContentAlignment="Left">
<ScrollViewer >
<Frame Name="LevelsContent" Source="LevelsTab.xaml"/>
</ScrollViewer>
</TabItem>
The LevelsTab.xaml UserControl is really barebone and just contains buttons to create or remove a custom UserControl I created to represent graphically a floor in the UI (screenshot below). This very simple as well:
LevelDefinition.xaml
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition" ....
<Label Grid.Column="0" Content="Level:"/>
<TextBox Name="LevelName" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="LevelElevation" Text={Binding <!--yet to be bound-->}/>
<TextBox Name="ToFloorAbove" Text={Binding <!--yet to be bound-->}/>
</UserControl>
When the user clicks the buttons to add or remove floors in LevelsTab.xaml, a new LevelDefinition is added or removed to the gird.
Each LevelDefinition will be able to create a Level object from the information contained in the different TextBox elements, using MVVM. Eventually, in the ViewModel, I should have a List<Level> I guess.
Level.cs
class Level
{
public double Elevation { get; set; }
public string Name { get; set; }
public string Number { get; set; }
}
Each LevelDefinition should be sort of bound to the previous one though, as the floor below contains the information of the height to the Level above. The right-most TextBox in LevelDefinition.xaml indicated the distance between the current floor and the floor above, hence the Height `TextBox should just be the sum of its height PLUS the distance to the level above:
Of course the extra level of difficulty here is that if I change distance to the level above in one floor, all the floors above will have to update the height. For example: I change LEVEL 01 (from the pic) to have 4 meters to the level above, LEVEL 02's height will have to update to become 7m (instead of 6) and LEVEL 03's will have to become 10m.
But at this point I'm very lost:
How do I get this logic of getting the floor height bound to the info in the floor below?
How do I implement MVVM correctly in this case?
I hope I managed to explain the situation correctly even though it's quite complex and thanks for the help!
If you intend to make your Level items editable, you have to implement INotifyPropertyChanged. I created a level view model for demonstration purposes and added a property OverallElevation that represents the current elevation including that of previous levels.
public class LevelViewModel : INotifyPropertyChanged
{
private string _name;
private int _number;
private double _elevation;
private double _overallElevation;
public LevelViewModel(string name, int number, double elevation, double overallElevation)
{
Number = number;
Name = name;
Elevation = elevation;
OverallElevation = overallElevation;
}
public string Name
{
get => _name;
set
{
if (_name == value)
return;
_name = value;
OnPropertyChanged();
}
}
public int Number
{
get => _number;
set
{
if (_number == value)
return;
_number = value;
OnPropertyChanged();
}
}
public double Elevation
{
get => _elevation;
set
{
if (_elevation.CompareTo(value) == 0)
return;
_elevation = value;
OnPropertyChanged();
}
}
public double OverallElevation
{
get => _overallElevation;
set
{
if (_overallElevation.CompareTo(value) == 0)
return;
_overallElevation = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You can bind these properties to your LevelDefinition user control. I adapted your sample, because it is incomplete. Since the overall elevation is calculated, I set the corresponding TextBox to be read-only, but you should really use a TextBlock or a similar read-only control instead.
<UserControl x:Class="RevitPrototype.Setup.LevelDefinition"
...>
<UserControl.Resources>
<Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="Margin" Value="5"/>
</Style>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="Level:"/>
<TextBox Grid.Column="1" Name="LevelName" Text="{Binding Name}"/>
<TextBox Grid.Column="2" Name="LevelElevation" Text="{Binding OverallElevation}" IsReadOnly="True"/>
<TextBox Grid.Column="3" Name="ToFloorAbove" Text="{Binding Elevation}"/>
</Grid>
</UserControl>
Since you did not provide your tab view model, I created one for reference. This view model exposes an ObservableCollection of levels, a GroundFloor property and commands to add and remove levels. I use a DelegateCommand type, but you may use a different one.
On each add of a level, you subscribe to the PropertyChanged event of the new level and on removal you unsubscribe to prevent memory leaks. Now, whenever a property changes on a LevelViewModel instance, the OnLevelPropertyChanged method is called. This method checks, if the Elevation property was changed. If it was, the UpdateOverallElevation method is called, which recalculates all overall elevation properties. Of course you could optimize this to only recalculate the levels above the current one passed as sender.
For a more robust implementation, you should subscribe to the CollectionChanged event of the Levels collection, so can subscribe to and unsubscribe from the PropertyChanged events of level items whenever you add, remove or modify the collection in other ways than through the commands like restoring a persisted collection.
public class LevelsViewModel
{
private const string GroundName = "GROUND FLOOR";
private const string LevelName = "LEVEL";
public ObservableCollection<LevelViewModel> Levels { get; }
public LevelViewModel GroundFloor { get; }
public ICommand Add { get; }
public ICommand Remove { get; }
public LevelsViewModel()
{
Levels = new ObservableCollection<LevelViewModel>();
GroundFloor = new LevelViewModel(GroundName, 0, 0, 0);
Add = new DelegateCommand<string>(ExecuteAdd);
Remove = new DelegateCommand(ExecuteRemove);
GroundFloor.PropertyChanged += OnLevelPropertyChanged;
}
private void ExecuteAdd(string arg)
{
if (!double.TryParse(arg, out var value))
return;
var lastLevel = Levels.Any() ? Levels.Last() : GroundFloor;
var number = lastLevel.Number + 1;
var name = GetDefaultLevelName(number);
var overallHeight = lastLevel.OverallElevation + value;
var level = new LevelViewModel(name, number, value, overallHeight);
level.PropertyChanged += OnLevelPropertyChanged;
Levels.Add(level);
}
private void ExecuteRemove()
{
if (!Levels.Any())
return;
var lastLevel = Levels.Last();
lastLevel.PropertyChanged -= OnLevelPropertyChanged;
Levels.Remove(lastLevel);
}
private void OnLevelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(LevelViewModel.Elevation))
return;
UpdateOverallElevation();
}
private static string GetDefaultLevelName(int number)
{
return $"{LevelName} {number:D2}";
}
private void UpdateOverallElevation()
{
GroundFloor.OverallElevation = GroundFloor.Elevation;
var previousLevel = GroundFloor;
foreach (var level in Levels)
{
level.OverallElevation = previousLevel.OverallElevation + level.Elevation;
previousLevel = level;
}
}
}
The view for the levels tab item could look like below. You can use a ListBox with your LevelDefinition user control as item template to display the levels. Alternatively, you could use a DataGrid with editable columns for each property of the LevelViewModel, which would be more flexible for users.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Levels}">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<local:LevelDefinition/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListView>
<DockPanel Grid.Row="1" Margin="5">
<Button DockPanel.Dock="Right" Content="-" MinWidth="50" Command="{Binding Remove}"/>
<Button DockPanel.Dock="Right" Content="+" MinWidth="50" Command="{Binding Add}" CommandParameter="{Binding Text, ElementName=NewLevelElevationTextBox}"/>
<TextBox x:Name="NewLevelElevationTextBox" MinWidth="100"/>
</DockPanel>
<local:LevelDefinition Grid.Row="2" DataContext="{Binding GroundFloor}"/>
</Grid>
This is a simplified example, there is no input validation, invalid values are ignored on adding.
I've managed to implement this using a multi-binding converter.
Assuming that you set up the multi-converter as a static resource somewhere, the TextBlock to display the value is:
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource ElevationMultiConverter}">
<MultiBinding.Bindings>
<Binding Path="" />
<Binding Path="DataContext.Levels" RelativeSource="{RelativeSource AncestorType={x:Type ItemsControl}}" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The converter itself looks like this:
class ElevationMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var item = values[0] as Level;
var list = values[1] as IList<Level>;
var lowerLevels = list.Where(listItem => list.IndexOf(listItem) <= list.IndexOf(item));
var elevation = lowerLevels.Sum(listItem => listItem.Height);
return elevation.ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
In this example, it depends on the specific order of items in the list to determine whether a level is above or below another; you could use a property, or whatever else.
I didn't use a framework for this example so I needed to implement INotifyPropertyChanged everywhere myself. In the MainViewModel, this meant adding a listener to each Level element's PropertyChanged event to trigger the multibinding converter to have 'changed'. In total, my MainViewModel looked like this:
class MainViewModel :INotifyPropertyChanged
{
public ObservableCollection<Level> Levels { get; set; }
public MainViewModel()
{
Levels = new ObservableCollection<Level>();
Levels.CollectionChanged += Levels_CollectionChanged;
}
private void Levels_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
foreach(var i in e.NewItems)
{
(i as Level).PropertyChanged += MainViewModel_PropertyChanged;
}
}
private void MainViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Levels)));
}
public event PropertyChangedEventHandler PropertyChanged;
}
How it works:
A new Level is added to the collection, and it's PropertyChanged event is listened to by the containing view model. When the height of a level changes, the PropertyChanged event is fired and is picked up by the MainViewModel. It in turn fires a PropertyChanged event for the Levels property. The MultiConverter is bound to the Levels property, and all changes for it trigger the converters to re-evaluate and update all of the levels combined height values.

Two-way binding and filtering of ObservableCollection in WPF/MVVM

I am learning MVVM pattern while refactoring an app to MVVM.
I have a model class Machine that provides a list of installations in a form of ObservableCollection<Installation> Installations.
In one of the windows (views) I need to display only those installations that have updates (thus meet the following criteria):
private void InstallationsToUpdateFilter(object sender, FilterEventArgs e)
{
var x = (Installation)e.Item;
bool hasNewVersion = ShowAllEnabledInstallations ? true : x.NewVersion != null;
bool isSetAndOn = !String.IsNullOrEmpty(x.Path) && x.CheckForUpdatesFlag;
e.Accepted = isSetAndOn && hasNewVersion;
}
private void OnFilterChanged()
{
installationsToUpdateSource?.View?.Refresh();
}
I am doing this by filtering in my ViewModel:
class NewVersionViewModel : ViewModelBase
{
private Machine machine = App.Machine;
...
public NewVersionViewModel(...)
{
...
InstallationsToUpdate.CollectionChanged += (s, e) =>
{
OnPropertyChanged("NewVersionsAvailableMessage");
OnFilterChanged();
};
installationsToUpdateSource = new CollectionViewSource();
installationsToUpdateSource.Source = InstallationsToUpdate;
installationsToUpdateSource.Filter += InstallationsToUpdateFilter;
}
public ObservableCollection<Installation> InstallationsToUpdate
{
get { return machine.Installations; }
set { machine.Installations = value; }
}
internal CollectionViewSource installationsToUpdateSource { get; set; }
public ICollectionView InstallationsToUpdateSourceCollection
{
get { return installationsToUpdateSource.View; }
}
...
}
This is done by custom ListView:
<ListView ItemsSource="{Binding InstallationsToUpdateSourceCollection}" ... >
...
<ListView.ItemTemplate>
<DataTemplate>
<Grid ...>
<Grid ...>
<CheckBox Style="{StaticResource LargeCheckBox}"
IsChecked="{Binding Path=MarkedForUpdate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding Path=HasNewVersion}"
/>
</Grid>
<Label Content="{Binding Path=InstalledVersion.Major}" Grid.Column="1" Grid.Row="0" FontSize="50" FontFamily="Segoe UI Black" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,-10,0,0"/>
...
<Grid.ContextMenu>
<ContextMenu>
...
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
All of this works - until I try to "send" <CheckBox IsChecked="{Binding Path=MarkedForUpdate... back to my model - so it will be stored there.
How it can be done? (Can I have some kind of setter on ICollectionView?)
Current architecture can be changed. What I ultimately need:
Display items (installations) from model in ListView (currently: works)
Filter/Show only installations that meet some criteria (currentrly: works)
Reflect changes in MarkedForUpdate checkbox back to model (currently: not working)
I've googled a lot but was unable to find a relevant solution or suggestions.
Any help would be greatly appreciated. Thanks!
I figured the problem out. Although it was a silly mistake, I still want to share it to save someone's time.
The model itself updates in the configuration described above. The problem was that what model property (Machine.Installations in my case) did not implement INotifyPropertyChanged interface so other Views (through their corresponding ViewModels) were not aware of changes. Thus one should use OnPropertyChanged/RaisePropertyChanged not only in ViewModel, but in Model as well.
Hope this may help someone.

Change the Background of an ELEMENT within a DataTemplate

I've looked online and found some topics related to my issue, although being new to XAML and WPF, i'm having trouble making what I want work.
I have a custom TimeLineControl StackPanel that contains 'Items' of type TimeLineFunctionControl, where the Items uses a DataTemplate to define how the 'Items' are displayed.
<!-- Static Resource = BgColor -->
<Color R="255" G="255" B="255" A="180" x:Key="BgColor" />
<!-- Static Resource = BgBrush -->
<SolidColorBrush Color="{DynamicResource BgColor}" x:Key="BgBrush" />
<!-- DataTemplate = TimeLineFunctionDataTemplate -->
<DataTemplate DataType="{x:Type tt:FunctionDataType}"
x:Key="TimeLineFunctionDataTemplate">
<Border x:Name="DataContainer"
BorderThickness="0.3"
BorderBrush="Black"
CornerRadius="2"
Margin="0,20,0,10"
Height="50"
Background="{DynamicResource BgBrush}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
<TextBlock Text="{Binding Path=StartTime.TotalMilliseconds, StringFormat={}{0} ms}" FontSize="8"/>
<TextBlock Text="{Binding Path=EndTime.TotalMilliseconds, StringFormat={}{0} ms}" FontSize="8"/>
</StackPanel>
</Border>
</DataTemplate>
public class FunctionDataType : ITimeLineDataItem
{
public TimeSpan? StartTime { get; set; }
public TimeSpan? EndTime { get; set; }
public Boolean TimelineViewExpanded { get; set; }
public String Name { get; set; }
}
I want to be able to change the Background color of the Border (DataContainer) dynamically from within the code.
I've tried the following;
1 - Doesn't work, I've since learnt that once a Template is applied, the Background property is no longer used.
titem.Background = (Brush)FindResource("BgBrushTriggered");
2 - Works, although I need to have defined two (2) DataTemplate in XAML, each with different Background colors, seems there must be a better way to do it.
titem.ContentTemplate = (DataTemplate)FindResource("TimeLineFunctionDataTriggeredTemplate");
3 - Works, although it changes ALL the items, since i'm changing the DynamicResource value.
this.Resources["BgBrush"] = new SolidColorBrush((Color)FindResource("BgColorTriggered"));
4 - Doesn't work, XAML reports "The member "Background" is not recognized or is not accessible";
Background="{TemplateBinding Background}"
Background="{Binding Background, RelativeSource={RelativeSource TemplatedParent}"
Q: What are my options?
Q: Is there a good online resource to correctly learn XAML and now to apply bindings, styles, templates etc...
The correct way to do this would be to bind the Background property of the Border to a property of the FunctionDataType objects and then set this property of the particular item you want to change.
You could either bind directly to a Brush property or define some other type of property and use a converter to convert this value into a Brush. The FunctionDataType must implement the INotifyPropertyChanged interface for this to work.
Please refer to the following sample code.
<DataTemplate DataType="{x:Type tt:FunctionDataType}"
x:Key="TimeLineFunctionDataTemplate">
<Border x:Name="DataContainer"
BorderThickness="0.3"
BorderBrush="Black"
CornerRadius="2"
Margin="0,20,0,10"
Height="50"
Background="{Binding BgBrush}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
<TextBlock Text="{Binding Path=StartTime.TotalMilliseconds, StringFormat={}{0} ms}" FontSize="8"/>
<TextBlock Text="{Binding Path=EndTime.TotalMilliseconds, StringFormat={}{0} ms}" FontSize="8"/>
</StackPanel>
</Border>
</DataTemplate>
public class FunctionDataType : ITimeLineDataItem, INotifyPropertyChanged
{
public TimeSpan? StartTime { get; set; }
public TimeSpan? EndTime { get; set; }
public Boolean TimelineViewExpanded { get; set; }
public String Name { get; set; }
private Brush _bgBrush;
public Brush BgBrush
{
get { return _bgBrush; }
set { _bgBrush = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
var item = titem.DataContext as FunctionDataType;
item.BgBrush = System.Windows.Media.Brushes.Red;
you can use a property in your model ItemColorIndex and then bind the background via a converter to it Background="{Binding ItemColorIndex , Converter=XXX}

How to know which element is tapped in ListView? [duplicate]

This question already has answers here:
How to get clicked item in ListView
(5 answers)
Closed 7 years ago.
I've got a ListView with a DataTemplate like this, using MVVM pattern
<ListView ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
commands:ItemsClickCommand.Command="{Binding ItemClickedCommand}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ItemsClickCommand is defined in this way
public static class ItemsClickCommand
{
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached("Command", typeof(BindableCommand), typeof(ItemsClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, BindableCommand value)
{
d.SetValue(CommandProperty, value);
}
public static BindableCommand GetCommand(DependencyObject d)
{
return (BindableCommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.ItemClick += OnItemClick;
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.OriginalSource))
command.ExecuteWithMoreParameters(e.OriginalSource, e.ClickedItem);
}
}
What I'm asking is how can I know if user tap on the TextBlock or Button.
I tried to handle ItemClickCommand event in this way in ViewModel to search controls in VisualTree (is this the best solution?), but the cast to DependencyObject doesn't work (returns always null)
public void ItemClicked(object originalSource, object clickedItem)
{
var source = originalSourceas DependencyObject;
if (source == null)
return;
}
There are a few solutions that come to mind
Solution 1
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button
Content="{Binding B}"
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Note how the ListView has the name set to "parent" with the attribute: x:Name="parent" and how the binding for the button's command uses that. Also note that the command will be provided with a parameter that is the reference to the data source for the element that was clicked.
The view model for this page will look like this:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}
'MvxCommand' is just a particular implementation of ICommand. I used MvvMCross for my sample code but you don't have to do that - you can use whatever MvvM implementation you need.
This solution is appropriate if the responsibility to handle the command lies with the view model for the page that contains the list.
Solution 2
Handling the command in the view model for the page that contains the list may not always be appropriate. You may want to move that logic in code that is closer to the element that is being clicked. In that case, isolate the data template for the element in its own user control, create a view model class that corresponds to the logic behind that user control and implement the command in that view model. Here is how the code would look like:
The XAML for the ListView:
<ListView
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<uc:MyElement DataContext="{Binding Converter={StaticResource MySourceToMyElementViewModelConverter}}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The XAML for the user control representing one element:
<Grid>
<StackPanel>
<TextBlock Text="{Binding Source.A}" />
<Button Content="{Binding Source.B}" Command="{Binding BCommand}" />
</StackPanel>
</Grid>
The source code for MySourceToMyElementViewModelConverter:
public class MySourceToMyElementViewModelConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return new MyElementViewModel((MySource)value);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model for the main page:
public class MainViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MainViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
}
}
The view model for the user control representing one element in the list:
public class MyElementViewModel : MvxViewModel
{
public MySource Source { get; private set; }
public MvxCommand BCommand { get; private set; }
public MyElementViewModel(MySource source)
{
Source = source;
BCommand = new MvxCommand(ExecuteBCommand);
}
private void ExecuteBCommand()
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", Source.A, Source.B);
}
}
Solution 3
Your sample assumes that the view model for the main page exposes a list of data model elements. Something like this:
public ObservableCollection<MySource> Source { get; private set; }
The view model for the main page could be changed so that it exposes a list of view model elements instead. Something like this:
public ObservableCollection<MyElementViewModel> ElementViewModelList { get; private set; }
Each element in ElementViewModelList would correspond to an element in Source. This solution can get slightly complex if the contents of Source changes at run time. The view model of the main page will need to observe Source and change ElementViewModelList accordingly. Going further don this path you may want to abstract the concept of a collection mapper (something similar with an ICollectionView) and provide some generic code for doing so.
For this solution, the XAML will look like this:
<ListView
ItemsSource="{Binding ElementViewModelList}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<Button Content="{Binding B}" Command="{Binding BCommand}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Notes for Solution 1, 2 and 3
I see that your original sample associates a commanding not with the button inside of the element but with the entire element. That raises the question: what are you going to do with the inner button? Will you have a situation where the user can click either on the element or on the inner button? That may not be the best solution as far as UI/UX goes. Be mindful of that. Just as an exercise and in order to get closer to your original sample, here is what you can do if you want to associate commanding with the entire element.
Wrap your entire element in a button with a custom style. That style will modify the way a click is handled visually. The simplest form of that is to have the click not create any visual effect. This change applied to Solution 1 (it can easily be applied to Solution 2 and Solution 3 as well) would look something like this:
<ListView
x:Name="parent"
ItemsSource="{Binding Source}"
IsItemClickEnabled="True"
Margin="20">
<ListView.ItemTemplate>
<DataTemplate>
<Button
Command="{Binding DataContext.BCommand, ElementName=parent}"
CommandParameter="{Binding}"
Style="{StaticResource NoVisualEffectButtonStyle}">
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</Button>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
In this case you would have to write NoVisualEffectButtonStyle but that is a simple task. You would also need to decide what kind of commanding you want to associate with the inner button (otherwise why would you have an inner button). Or, more likely you could transform the inner button in something like a textbox.
Solution 4
Use Behaviors.
First, add a reference to "Behaviors SDK".. Then modify your XAML code:
...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
...
<Grid>
<ListView ItemsSource="{Binding Source}" IsItemClickEnabled="True" Margin="20">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="ItemClick">
<core:InvokeCommandAction
Command="{Binding BCommand}"
InputConverter="{StaticResource ItemClickedToMySourceConverter}" />
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding A}" />
<TextBlock Text="{Binding B}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ItemClickedToMySourceConverter is just a normal value converter:
public class ItemClickedToMySourceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return (MySource)(((ItemClickEventArgs)value).ClickedItem);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotSupportedException();
}
}
The view model will look like this:
public class Main4ViewModel : MvxViewModel
{
public ObservableCollection<MySource> Source { get; private set; }
public MvxCommand<MySource> BCommand { get; private set; }
public Main4ViewModel()
{
Source = new ObservableCollection<MySource>()
{
new MySource("e1", "b1"),
new MySource("e2", "b2"),
new MySource("e3", "b3"),
};
BCommand = new MvxCommand<MySource>(ExecuteBCommand);
}
private void ExecuteBCommand(MySource source)
{
Debug.WriteLine("ExecuteBCommand. Source: A={0}, B={1}", source.A, source.B);
}
}

Dynamically displaying Items using FlipView and DataTemplateSelector in WinRT

I'm using Flipview and a DataTemplateSelector to determine at runtime which DataTemplate to apply to show items in my control.
I have two DataTemplate's, one is static and the second can be used by a undetermined number of items.
Currently
My first view displays:
- "This is a test - Content"
Followed by 18 other views that look like this:
- "http://www.google.com/ 0"
- "http://www.google.com/ 1"
- "http://www.google.com/ 2"
- and so on until 17
I want
The items "http://www.google.com/ " to be grouped as 3 on a view.
For example the second view will display:
"http://www.google.com/ 0, http://www.google.com/ 1, http://www.google.com/ 2"
The third view will display:
"http://www.google.com/ 3, http://www.google.com/ 4, http://www.google.com/ 5"
And so on..
Bellow is my code:
FlipViewDemo.xaml
<Page.Resources>
<DataTemplate x:Key="FirstDataTemplate">
<Grid>
<TextBlock Text="{Binding Content}" Margin="10,0,18,18"></TextBlock>
</Grid>
</DataTemplate>
<DataTemplate x:Key="SecondDataTemplate">
<TextBox Text="{Binding Url}"></TextBox>
</DataTemplate>
<local:MyDataTemplateSelector x:Key="MyDataTemplateSelector"
FirstTextTemplate="{StaticResource FirstDataTemplate}"
SecondTextTemplate="{StaticResource SecondDataTemplate}">
</local:MyDataTemplateSelector>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<FlipView x:Name="itemGridView" ItemTemplateSelector="{StaticResource MyDataTemplateSelector}"
Margin="265,220,284,162">
</FlipView>
</Grid>
FlipViewDemo.xaml.cs
public sealed partial class FlipViewDemo : Page
{
public FlipViewDemo()
{
this.InitializeComponent();
var items = new List<BaseClass>();
items.Add(new FirstItem
{
Content="This is a test - Content"
});
for (int i = 0; i < 18; i++)
{
items.Add(new SecondItem
{
Url = "http://www.google.com/ " + i.ToString()
});
}
itemGridView.ItemsSource = items;
}
}
public class BaseClass
{
}
public class FirstItem : BaseClass
{
public string Content { get; set; }
}
public class SecondItem : BaseClass
{
public string Url { get; set; }
}
public class MyDataTemplateSelector : DataTemplateSelector
{
public DataTemplate FirstTextTemplate { get; set; }
public DataTemplate SecondTextTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item,
DependencyObject container)
{
if (item is FirstItem)
return FirstTextTemplate;
if (item is SecondItem)
return SecondTextTemplate;
return base.SelectTemplateCore(item, container);
}
}
I'm thinking that maybe this can be achieved with groups and list view. But I'm not sure how this can be done.
Probably it is a stupid question but, using Google, I can't find an answer. Also english is not my native language; please excuse typing errors.
I think the way to achieve what you are looking for is to expose the data in a way that better represents what you want to display. Then, you can use nested controls to display it. I just threw this together (using my own test data). It is probably not exactly what you want, but it should help you figure things out.
ViewModel
Here I made a helper method to build the collection with sub-collections that each have 3 items.
class FlipViewDemo
{
private List<object> mData;
public IEnumerable<object> Data
{
get { return mData; }
}
public FlipViewDemo()
{
mData = new List<object>();
mData.Add("Test String");
for (int i = 0; i < 18; ++i)
{
AddData("Test Data " + i.ToString());
}
}
private void AddData(object data)
{
List<object> current = mData.LastOrDefault() as List<object>;
if (current == null || current.Count == 3)
{
current = new List<object>();
mData.Add(current);
}
current.Add(data);
}
}
class TemplateSelector : DataTemplateSelector
{
public DataTemplate ListTemplate { get; set; }
public DataTemplate ObjectTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is List<object>) return ListTemplate;
return ObjectTemplate;
}
}
Xaml
Here I use an ItemsControl to vertically stack the items in the data. Each item is either a list of three objects or a single object. I use a FlipView for each of the lists of three objects and a simple ContentPresenter for the single objects.
<Page.Resources>
<DataTemplate x:Key="ListTemplate">
<FlipView
ItemsSource="{Binding}">
<FlipView.ItemTemplate>
<DataTemplate>
<ContentPresenter
Margin="0 0 10 0"
Content="{Binding}" />
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
</DataTemplate>
<DataTemplate x:Key="ObjectTemplate">
<ContentPresenter
Margin="0 0 10 0"
Content="{Binding}" />
</DataTemplate>
<local:TemplateSelector
x:Key="TemplateSelector"
ListTemplate="{StaticResource ListTemplate}"
ObjectTemplate="{StaticResource ObjectTemplate}" />
</Page.Resources>
<ItemsControl
ItemsSource="{Binding Data}"
ItemTemplateSelector="{StaticResource TemplateSelector}" />
Note: You usually would not need a template selector for something like this, but since you need to select between a List<T> and an Object, there is no way I know of to recognize the difference using only the DataTemplate.TargetType property from Xaml due to List<t> being a generic type. (I tried {x:Type collections:List`1} and it did not work.)
You need to group items in viewmodel, and databind ItemsSource to the groups. In flipview's itemtemplate you display items in group.
public class PageGroup : PageBase {
public ObservableColection<BaseClass> Items { get; set; }
}
public ObservableCollection<PageBase> Pages { get; set; }
<FlipView ItemsSource="{Binding Pages}">
<FlipView.ItemTemplate>
<DataTemplate DataType="local:PageGroup">
<ItemsControl ItemsSource="{Binding Items}"
ItemTemplateSelector="{StaticResource MyDataTemplateSelector}" />
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
In order to display first page differently from others:
public class FirstPage : PageBase {
public string Title { get; }
}
Pages.Insert(0, new FirstPage());
and you need to use another datatemplaeselector or impicit datatemplates in FlipView to differentiate between FirstPage and PageGroup
<FlipView ItemsSource="{Binding Pages}"
ItemTemplateSelector="{StaticResource PageTemplateSelector}" />
You don't need to worry about selecting the appropriate template based on the class type, you can simply define the class in the DataTemplate itself.
<DataTemplate TargetType="{x:Type myNamespace:FirstItem}">
...
</DataTemplate>
You'll need to specify where the class is by adding the namespace at the top of your page:
xmlns:myNamespace="clr-namespace:MyApp.MyNamespace"

Categories