I'm trying to further customize build-in capability of WPF ListBox for showing items in groups.
In short, I want to hide Group's container (and Group's title altogether) if all items inside group are collapsed (Visibility property).
First, I have very simple class City that represent single Item. This class include Shown property. Inside ItemContainerStyle I simply have DataTrigger that set Visibility to Collapsed if value of this property is False.
class City : INotifyPropertyChanged
{
private bool m_Shown = true;
public string Name { get; set; }
public string Country { get; set; }
public bool Shown
{
get
{
return m_Shown;
}
set
{
m_Shown = value;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Shown"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
This is how I add sample cities, add Group description and all work fine.
m_cities = new List<City>
{
new City() { Name = "Berlin", Country = "Germany" },
new City() { Name = "Milano", Country = "Italy" },
new City() { Name = "Frankfurt", Country = "Germany" },
new City() { Name = "Rome", Country = "Italy" }
};
ICollectionView view = CollectionViewSource.GetDefaultView(m_cities);
view.GroupDescriptions.Add(new PropertyGroupDescription("Country"));
Cities = view; // <-- Binds to ItemsSource of ListBox
I tried in several ways to automatically hide Group if there are no more items visible in it (all are collapsed), but all without luck.
One way is to repeat last 3 lines in code above and this works, but I noticed slowdown with this method and listbox must work fast for user.
Bellow is one of my examples and this actually worked for hiding, but I can't bring group to be visible anymore after that. I tried with converters and similar, but I can't get group visible again.
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Style.Triggers>
<Trigger Property="ActualHeight" Value="20">
<Setter Property="Visibility" Value="Collapsed"/>
</Trigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel>
<TextBlock Text="{Binding Path=Name}"/>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
Thanks for any help.
A bit (!) late with this, but hopefully it might help someone else in the future.
Inside the control template of most (all?) GroupItem styles is an ItemsPresenter that is used to host and display the child items that belong to the group. It stands to reason that, if all of the child items are collapsed, this ItemsPresenter will have a height of zero.
Therefore, you can add a trigger to the control template based on this condition, and set the Visibility of the whole group item accordingly. A normal property trigger doesn't seem to work, but a data trigger will. Something like this:
<ControlTemplate>
<StackPanel x:Name="Root">
...
<ItemsPresenter x:Name="ItemsPresenter" />
...
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ActualHeight, ElementName=ItemsPresenter}" Value="0">
<Setter TargetName="Root" Property="Visibility" Value="Collapsed" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
You will need to name the root element of the control template (in this example it's a StackPanel element I've named "Root") and also the ItemsPresenter element (I've just called it "ItemsPresenter"). Obviously the root element might be a different type, and you can use whichever names you like.
You were on the right track, but you needed to bind to the ActualHeight of the ItemsPresenter, and it needed to be a data trigger not a normal property trigger.
Related
I have this code in my xaml which says to color my button when I hover my mouse and click my mouse over the button.
<Border x:Class="DatasetGrid.RowHeaderButton"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" MinWidth="30" Width="Auto">
<Border.Resources>
<SolidColorBrush x:Key="ButtOverBrush" Color="#53C3D5" Opacity="0.2"></SolidColorBrush>
<SolidColorBrush x:Key="ButtPressedBrush" Color="#53C3D5" Opacity="0.5"></SolidColorBrush>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource ButtOverBrush}"></Setter>
</Trigger>
<DataTrigger Binding="{Binding IsMouseDown, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
This works all well and good, but I find that as soon as I change the Background color in code behind, the above MouseOver and MouseDown triggers don't fire anymore.
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
I'm quite new to WPF so I'm not sure what's going wrong.
Edit:
So to give some more information, my control above is a RowHeaderButton, i.e the row header to a grid. Each row in the grid has it's own row header button. So when the user hovers over or clicks it, it should change from white to the specified SolidColorBrush above.
In the code behind of another control, DataGrid.xaml.cs, I have the below code (simplified) which will change the color of the row header when when a cell in the same row of the grid is selected or not.
void UpdateSelectedCells() {
foreach (Cell cell in VisibleColumns.SelectMany(c => c.VisibleCells))
{
int cellRowIndex = cell.CellInfo.RowIndex;
cell.IsSelected = SelectedCells.Contains(cell.CellInfo);
foreach (RowHeaderButton rhb in RowHeadersColumn.VisibleRowHeaders)
{
int rowHeaderIndex = Convert.ToInt16(rhb._default.Text) - 1;
if (cellRowIndex == rowHeaderIndex)
{
if (cell.IsSelected)
{
rhb.Background = new SolidColorBrush(Color.FromArgb(100, 83, 195, 213));
}
else
{
bool rowselected = false;
//need to check if any other cell in the row is selected, if not then color row header white
foreach (CellInfo celll in SelectedCells)
{
if (celll.RowIndex == cellRowIndex)
{
rowselected = true;
break;
}
}
if (rowselected == false)
rhb.Background = Brushes.White;
}
}
}
}
}
I don't have a ViewModel for this.
The triggers are firing, but their setters are being overridden.
This is due to Dependency Property Value Precendence. If the Background property is set programmatically or as an attribute in the XAML, that value will override anything value any style setter gives it. In general, this is desirable behavior: You want to be able to override what the style does on an individual control.
The solution to this is to do all of your background brush changes in style triggers. Your code behind must have some reason for setting the background brush when it does. Whatever that is, find a way to do it with a trigger. Set a property on the viewmodel and write a trigger on that property.
If you need help translating that high level abstraction into your own code, please share enough code for me to understand why and where the codebehind is setting the Background, and what (if anything) you have for a viewmodel.
I solved the issue by creating a new Dependancy Property and binding it to a data trigger.
public bool IsCellSelected
{
get { return (bool)GetValue(IsCellSelectedProperty); }
set { SetValue(IsCellSelectedProperty, value); }
}
public static readonly DependencyProperty IsCellSelectedProperty =
DependencyProperty.Register("IsCellSelected", typeof(bool), typeof(RowHeaderButton), new PropertyMetadata(null));
In my xaml I have:
<DataTrigger Binding="{Binding IsCellSelected, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="{StaticResource ButtPressedBrush}"></Setter>
</DataTrigger>
And in my code behind I set the value using:
RowHeaderButton rhb = RowHeadersColumn.VisibleRowHeaders[cell.CellInfo.RowIndex];
rhb.IsCellSelected = true; //or false
Now my button hover and button click events are not overridden.
I've been set to maintain a wpf application where there is a listbox for logging purposes.
The items displayed using listbox are of type TextMessage, i.e. the listbox is bound to these text messages via
ObservableCollection<TextMessage> Messages;
listBox.DataContext = Messages;
Messages are then added with something like
Messages.Add(new TextMessage("Test", TypeOfMessage.Headline));
This is the definition of the class TextMessage
public enum TypeOfMessage
{
Normal,
Headline,
Focus,
Important,
Fail,
Success
}
public class TextMessage
{
public TextMessage(string content, TypeOfMessage typeOfMessage)
{
Content = content;
TypeOfMessage = typeOfMessage;
CreationTime = DateTime.Now;
}
public string Content { get; }
public TypeOfMessage TypeOfMessage { get; }
public DateTime CreationTime { get; }
}
The xaml definition for the listbox is something like this:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="196" Margin="101,77,0,0" VerticalAlignment="Top" Width="256" ItemsSource="{Binding}" SelectionMode="Multiple">
<ListBox.InputBindings>
<KeyBinding
Key="C"
Modifiers="Control"
Command="Copy"
/>
</ListBox.InputBindings>
<ListBox.CommandBindings>
<CommandBinding
Command="Copy"
Executed="DoPerformCopy"
/>
</ListBox.CommandBindings>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock x:Name="TextToShow" Text="{Binding Content}"></TextBlock>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Normal">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Focus">
<Setter TargetName="TextToShow" Property="Foreground" Value="Black"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Headline">
<Setter TargetName="TextToShow" Property="Foreground" Value="RoyalBlue"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Important">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Fail">
<Setter TargetName="TextToShow" Property="Foreground" Value="Red"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding TypeOfMessage}" Value="Success">
<Setter TargetName="TextToShow" Property="Foreground" Value="Green"/>
<Setter TargetName="TextToShow" Property="FontWeight" Value="Bold"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
This works nicely (i.e messages are displayed in the listbox in different font weight and color depending on their type), but now for the question :
Is there any way using BindingExpression or any other means to get the font formatting and coloring from code behind from the xaml definitions ?
The reason is that I want to just have the formatting in one place (just in the xaml as it is right now) but still be able to reuse it when I want to copy the contents (using code behind) including font formatting to the clipboard.
Example:
private void DoPerformCopy()
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
TextPointer startPos = rtb.CaretPosition;
rtb.AppendText(message.Content);
rtb.Selection.Select(startPos, rtb.CaretPosition.DocumentEnd);
//
// Here it would be very nice to instead having multiple switch statements to get the formatting for the
// TypeOfMessage from the xaml file.
SolidColorBrush scb = new SolidColorBrush(message.TypeOfMessage == TypeOfMessage.Fail ? Colors.Red);
//
rtb.Selection.ApplyPropertyValue(RichTextBox.ForegroundProperty, scb);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}
Since I'm new to wpf, I'd really appreciate if someone has a tip for solving this. (I've tried hard to find an solution here at stackoverflow, but so far I've been unsuccessful)
Thanks in advance,
King regards
Magnus
Make a ContentPresenter with Content set to your TextMessage. Set the ContentTemplate to listBox.ItemTemplate and apply the template. It will create the visuals (TextBlock in this case). Then, just parse off the values from the TextBlock.
Also, your RichTextBox selection code wasn't working quite right so I fixed that by just inserting TextRanges to the end of it instead of trying to get the selection right.
private void DoPerformCopy(object sender, EventArgs e)
{
RichTextBox rtb = new RichTextBox();
foreach (TextMessage message in (listBox as ListBox)?.SelectedItems.Cast<TextMessage>().ToList())
{
ContentPresenter cp = new ContentPresenter();
cp.Content = message;
cp.ContentTemplate = listBox.ItemTemplate;
cp.ApplyTemplate();
var tb = VisualTreeHelper.GetChild(cp, 0) as TextBlock;
var fg = tb.Foreground;
var fw = tb.FontWeight;
var tr = new TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd);
tr.Text = message.Content;
tr.ApplyPropertyValue(RichTextBox.ForegroundProperty, fg);
tr.ApplyPropertyValue(RichTextBox.FontWeightProperty, fw);
}
// Now copy the whole thing to the Clipboard
rtb.Selection.Select(rtb.Document.ContentStart, rtb.Document.ContentEnd);
rtb.Copy();
}
In my WPF application, using MVVM Light, I use a DataGrid to display datas from a specific object collection.
As requirement I had seperated the headers in a first list, and the complete object collection in another one.
After reading lots of tutorials, posts, blogs... it appeared that ErrorDataInfo has to be placed in the ViewModel to manage the model properties error.
Here, as a try, I want to display a very basic error as you'll see in the code.
The app process is that the user choose a csv file, this file is converted to my object collection and then displayed in the DataGrid. The cells on error state should be displayed at this time (and the error message in a ToolTip).
I've tried many things but my DataGrid is still not displaying cells in errors.
Here is my ViewModel code :
public class ImportViewModel : ViewModelBase, IDataErrorInfo
{
#region Validation
public string Error
{
get
{
return string.Empty;
}
}
public string this[string columnName]
{
get
{
string result = null;
if (null != ListOfObjects)
{
if (_imp.ListOfHeaders.Where(x => x.Name == "EAN Code")
.ToString() == columnName)
if (ListOfObjects.Select(x => x.ean_cod).Single().Length > 3)
result = "Code must not be more than 3 digits";
}
return result;
}
}
And my View xaml code
<DataGrid Grid.Row="1"
AutoGenerateColumns="True"
ItemsSource="{Binding ListOfObjects,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnExceptions=True,
NotifyOnValidationError=True}">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
</Grid>
Thx for your help.
I am using a WPF treeview, when i click on a node\item once it gets selected. When the user clicks on the selected node the second time i want this node\item to get deselected i.e. i should be able to get the event. IsSelected is not called if i click on the selected node\item that is already selected. How do i get it to work?
<TreeView Grid.Column="0" Grid.Row="1" ItemsSource="{Binding source}" Name="mytreeview">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding displaytext}"/>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
and in my view model i have
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
if (value != _isSelected)
{
_isSelected = value;
if (_isSelected)
{
//my logic
}
this.OnPropertyChanged("IsSelected");
}
}
}
if (value != _isSelected)
Assuming that the UI is even trying to set something, that line is blocking your toggle logic. Something like this should fix at least that part.
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
else if(_isSelected)
{
IsSelected = false;
}
}
Otherwise the UI is checking the selection before setting the value and you'll need to handle it through some other user interaction like handling deselection on click.
I know this is a bit late but I've recently had the same requirement (i.e. unselecting a selected TreeViewItem on the second click) and I solved it by declaring an event handler for the 'MouseLeftButtonUp' event in a 'Style' entry for the ItemContainerStyle of the TreeView as follows:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="MouseLeftButtonUp" Handler="TreeViewItem_MouseLeftButtonUp"/>
</Style>
</TreeView.ItemContainerStyle>
The event handler in the code behind was as follows:
private TreeViewItem prevTVI;
private void TreeViewItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = (TreeViewItem)sender;
if (tvi == this.prevTVI)
{
this.prevTVI = null;
if (tvi.IsSelected)
tvi.IsSelected = false;
}
else
this.prevTVI = tvi;
e.Handled = true;
}
Now, I would like to ask if anyone thinks this approach breaks the MVVM pattern? I personally don't think so as the event handler is only concerned with the View and its objects not anything else but I would like to hear what others have to say, especially if someone has an alternative.
The IsSelected property is only changed when you select a new item. Clicking on the same item twice will normally have no effect. You would need to register the MouseDown event on the TreeView, and then force the item to be deselected in the code-behind.
I have one TextBlock having width say 100. When the text length is a large one I want to show the characters that is accomodated in that textblock and a (...) button besides the text to specify user that more text is also there. Upon click on that (...) button, the full text will be shown in a separate pop up window.
So i want how the dynamic (...) button will be shown whenever the text length exceed the size of the textblock. Please answer
This isn't exactly what you want, but it's a similar idea and just uses the baked-in stuff:
<TextBlock MaxWidth="200"
Text="{Binding YourLongText}"
TextTrimming="WordEllipsis"
ToolTip="{Binding YourLongText}" />
So you have a TextBlock with a maximum width, and when the text can't fit it displays an ellipsis ("..."). Hovering over the TextBlock with your mouse will show the full text in a ToolTip.
Just experience the same requirement for adding ellipsis on button so adding the solution here
<Style x:Key="editButton" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" >
<ContentPresenter.Resources>
<Style TargetType="TextBlock">
<Setter Property="TextTrimming" Value="CharacterEllipsis"></Setter>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Transparent"/>
</Trigger>
</Style.Triggers>
</Style>
Notice the resources in content presenter.
I believe what you want is to set the TextTrimming property. Settng it to WordElilipsis or CharacterEllipsis should provide what you need.
My solution to the problem is probably overkill, but allows for some configuration and control.
I created a behavior that allows me to set the character limit for each binding.
internal class EllipsisStringBehavior
{
public static readonly DependencyProperty CharacterLimitDependencyProperty = DependencyProperty.RegisterAttached("CharacterLimit", typeof(int), typeof(EllipsisStringBehavior), new PropertyMetadata(255, null, OnCoerceCharacterLimit));
public static readonly DependencyProperty InputTextDependencyProperty = DependencyProperty.RegisterAttached("InputText", typeof(string), typeof(EllipsisStringBehavior), new PropertyMetadata(string.Empty, OnInputTextChanged));
// Input Text
public static string GetInputText(DependencyObject dependencyObject)
{
return Convert.ToString(dependencyObject.GetValue(InputTextDependencyProperty));
}
public static void SetInputText(DependencyObject dependencyObject, string inputText)
{
dependencyObject.SetValue(InputTextDependencyProperty, inputText);
}
// Character Limit
public static int GetCharacterLimit(DependencyObject dependencyObject)
{
return Convert.ToInt32(dependencyObject.GetValue(CharacterLimitDependencyProperty));
}
public static void SetCharacterLimit(DependencyObject dependencyObject, object characterLimit)
{
dependencyObject.SetValue(CharacterLimitDependencyProperty, characterLimit);
}
private static void OnInputTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textblock = (TextBlock)d;
string input = e.NewValue == null ? string.Empty : e.NewValue.ToString();
int limit = GetCharacterLimit(d);
string result = input;
if (input.Length > limit && input.Length != 0)
{
result = $"{input.Substring(0, limit)}...";
}
textblock.Text = result;
}
private static object OnCoerceCharacterLimit(DependencyObject d, object baseValue)
{
return baseValue;
}
}
I then simply add the using to my user control...
<UserControl
xmlns:behavior="clr-namespace:My_APP.Helper.Behavior"
d:DesignHeight="300" d:DesignWidth="300">
...and apply the behavior to the TextBlock control I wish to use it on.
<TextBlock Margin="0,8,0,8"
behavior:EllipsisStringBehavior.CharacterLimit="10"
behavior:EllipsisStringBehavior.InputText="{Binding Path=DataContext.FeedItemTwo.Body, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource MaterialDesignSubheadingTextBlock}"
FontSize="14"/>
Hope this helps.