Hiding expander when all content is collapsed - c#

I have A WPF Datagrid that has a Collection View Source with 3 levels of grouping on it.
I have styled the datagrid to use 3 expanders such that it looks like this:
Level 1 Expander
<content>
Level 2 Expander
<content>
Level 3 Expander
<content>
Level 2 and Level 1 are just title of the groups
I have a second control that allows the user to show and hide level 3 items which works by binding the Level 3 expander to a Boolean "IsVisible" property in the object behind.
<!-- Style for groups under the top level. this is the style for how a sample is displayed -->
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<!-- The parent control that determines whether or not an item needs to be displayed. This holds all of the sub controls displayed for a sample -->
<Expander Margin="2"
Background="{Binding Path=Name,
Converter={StaticResource SampleTypeToColourConverter}}"
IsExpanded="True"
Visibility="{Binding Path=Items[0].IsVisibleInMainScreen,
Converter={StaticResource BoolToVisibilityConverter}}">
This approach works fantasically well.
HOWEVER
If the user deselects all items in a level 3 expander, the Level 2 expander header still displays meaning that valuable real estate is used up showing the header of a group with no visible data.
What I would like is a way to bind the visibility of the level 2 expander to its child controls and say "If all children are visible then show the expander, otherwise collapse it"
Is this possible?

I found a rather simple and clean way, yet not perfect, to achieve your goal. This should do the trick if hou don't have too much groups.
I've just added this trigger to the GroupItem ControlTemplate :
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=IP, Path=ActualHeight}" Value="0">
<Setter Property="Visibility" Value="Hidden"/>
<Setter Property="Height" Value="1"/>
</DataTrigger>
</ControlTemplate.Triggers>
When the ItemsPresenter (IP) ActualSize drops to zero, it Will almost collapse the header.
Why almost ?
When the control gets initialized and before the binding occurs, the ItemPresenter ActualHeight is 0 and when Visibility is set to Collapsed, the ItemPresenter doesn't get rendered at all.
Using Visibility.Hidden allows the ItemsPresenter to go to the render phase and be mesured.
I succedeed to drop Height to .4 px but I suspect this to be device dependant.

Assuming that you are using an MVVM sort of style, you could bind instead to a property of your group object that returns false if all of the children are invisible:
public bool AreChildrenVisible { get { return _children.Any(x=>x.IsVisibleInMainScreen); } }
Alternatively, pass the collection of Items through a Converter class to return Visibility depending on the aggregate status of all the subItems in the group.

This isn't a direct answer as you would have to implement it specifically for your needs but previously I have used a an override of the Grid Control to create dynamic grid allocation of members, if there are no visible members it then hides the parent group box.
public class DynamicLayoutGrid : Grid
{
protected override void OnInitialized(EventArgs e)
{
//Hook up the loaded event (this is used because it fires after the visibility binding has occurred)
this.Loaded += new RoutedEventHandler(DynamicLayoutGrid_Loaded);
base.OnInitialized(e);
}
void DynamicLayoutGrid_Loaded(object sender, RoutedEventArgs e)
{
int numberOfColumns = ColumnDefinitions.Count;
int columnSpan = 0;
int rowNum = 0;
int columnNum = 0;
int visibleCount = 0;
foreach (UIElement child in Children)
{
//We only want to layout visible items in the grid
if (child.Visibility != Visibility.Visible)
{
continue;
}
else
{
visibleCount++;
}
//Get the column span of the element if it is not in column 0 as we might need to take this into account
columnSpan = Grid.GetColumnSpan(child);
//set the Grid row of the element
Grid.SetRow(child, rowNum);
//set the grid column of the element (and shift it along if the previous element on this row had a rowspan greater than 0
Grid.SetColumn(child, columnNum);
//If there isn't any columnspan then just move to the next column normally
if (columnSpan == 0)
{
columnSpan = 1;
}
//Move to the next available column
columnNum += columnSpan;
//Move to the next row and start the columns again
if (columnNum >= numberOfColumns)
{
rowNum++;
columnNum = 0;
}
}
if (visibleCount == 0)
{
if (this.Parent.GetType() == typeof(GroupBox))
{
(this.Parent as GroupBox).Visibility = Visibility.Collapsed;
}
}
}
}

Use IMultiValueConverter implementation to convert items to visibility.
If all items IsVisibleInMainScreen property return true the converter will return visible else hidden.
Use the converter in the same place U used to convert the first item in original example

Related

How can I make a multi-colored segmented progress bar in wpf?

I'm trying to create a UserControl that acts as a sort of segmented progress bar. Input would be a collection of objects, each object would have a category, a duration property, and status property. The UserControl should stretch the width and height of the parent control. Each item in the collection should represent a segment of the progress bar; color of the segment is related to the status, the width of the segment is related to the duration, and the text overlaid on the segment would be related to the category or something.
Example custom progress bar:
The text might be the collection item's ID, the top segment color would be related to status, the bottom color would be related to the category, and the width related to the duration.
Some of the options I've considered:
Make a stackpanel and somehow define each items width and wrap the whole thing in a viewbox to make it stretch the height and width. How could I control the text size, how do I make the content fit the height, how do I bind a stackpanel to a collection?
Make an attached property for a grid control that would dynamically create columns and map the collection items to the grids. Seems like a lot of work and I'm hoping theres a simpler solution since my requirements are pretty specific.
Maybe theres a way to override a uniform grid to make it non-uniform?
Maybe I should just go all code-behind and draw rectangles by iterating through my collection?
Either way, I am crossing my fingers that somebody might know a simple solution to my problem.
Here is a full working proposition of solution to the custom progress bar.
Code is here : http://1drv.ms/1QmAVuZ
1 . If all the steps are not the same width, I prefer to use Grid with columns and different widths
The columns are built dynamically based upon following class :
public class StepItem
{
public int Length { get; set; }
public int Index { get; set; }
public String Label { get; set; }
public Brush Brush { get; set; }
}
2. I chose to implement a CustomControl and inherit of ItemsControl
CustomControl because I don't want to take care of implementing of the parts of the template of the Progressbar.
ItemsControl because :
-I want to provide to ItemsSource property a collection of StepItems
-ItemsControl can have some DataTemplate as template for each item
-ItemsControl can have any Panel like Grid as template presenting the collection of items
3. The component has template in Generic.xaml
-layoutGrid wil have the "continuous rainbow"
-overlayGrid will be displayed partially over the steps depending on progression or totally over (if no progress)
-ItemsPresenter will present the collection of DataTemplates corresponding to each StepItem
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ProgressItemsControl}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid x:Name="layoutGrid">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<Grid x:Name="overlayGrid" Width="100" HorizontalAlignment="Right" Background="White"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
4. Customisation of the ItemsPanel to use a Grid (instead of vertical layout)
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate >
<Grid x:Name="stepsGrid" IsItemsHost="True" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
5. In code behind of components, setting of the column width
int i = 0;
foreach (StepItem stepItem in ItemsSource)
{
total += stepItem.Length;
var columnDefinition = new ColumnDefinition() { Width = new GridLength(stepItem.Length, GridUnitType.Star) };
stepsGrid.ColumnDefinitions.Add(columnDefinition);
Grid.SetColumn(stepsGrid.Children[i], stepItem.Index);
i++;
}
6. Code behind for declaring Dependency properties that can be monitored
(excerpt)
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(ProgressItemsControl), new PropertyMetadata(0));
7. Usage of the component
<local:CustomProgressBar
x:Name="customProgressBar1"
HorizontalAlignment="Left" Height="50" Margin="32,49,0,0"
VerticalAlignment="Top" Width="379"/>
8. Feeding the component with data
private List<StepItem> stepItems = new List<StepItem>{
new StepItem{
Index=0,
Label="Step1",
Length=20,
Brush = new SolidColorBrush(Color.FromArgb(255,255,0,0)),
new StepItem{
Index=4,
Label="Step5",
Length=25,
Brush = new SolidColorBrush(Color.FromArgb(255,0,128,0)),
},
};
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
progressItemsControl1.ItemsSource = stepItems;
}
Regards

Couldn't get selected items correctly if setting ListView ItemsContainer as VirtualizationStackPanel

I set 'VirtualizingStackPanel.IsVirtualizing' to true and 'VirtualizingStackPanel.VirtualizationMode' to 'Recycling', because the items in my ListView are too many. The SelectionMode of the ListView is Extended, the 'IsSelected' property of the ListViewItem is bound to 'IsSelected' property of my model, bind mode is two way.
When I want to use Ctrl+A to select all of the items, it only select part of the items, so I use KeyBinding to write the select all method like below:
<KeyBinding Command="{Binding SelectAllCommand}"
Modifiers="Control"
Key="A"/>
SelectAll method will loop the ItemsSource collection and set each of the item's IsSelected property to true. But it also leads to something unexpected. When all of the items are selected, I scroll the scrollbar to the bottom and it will load more items to the ListView, I single click one item and the expected is all other items are unselected, only select this item. But, it seems not unselect other items.
Anybody can help?
This behaviour of the Selector is to be expected, because it can operate only with loaded UI elements. Due enabling virtualization you have loaded only those elements which contains in visible area. So, Selector does not "know" about others.
For fix this you must do so, that Selector "know" about previously selected items. In other word, you must to prohibit unloading any UI element which was selected.
First, create own virtualizing panel with blackjack and hookers:
public class MyVirtualizingStackPanel : VirtualizingStackPanel
{
protected override void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs e)
{
var item = e.UIElement as ListBoxItem;
if (item != null && item.IsSelected)
{
e.Cancel = true;
e.Handled = true;
return;
}
var item2 = e.UIElement as TreeViewItem;
if (item2 != null && item2.IsSelected)
{
e.Cancel = true;
e.Handled = true;
return;
}
base.OnCleanUpVirtualizedItem(e);
}
}
Next, replace default panel in the ListBox, ListView, TreeView or other user controls which provide selector.
For example, via style:
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<blackjackandhookers:MyVirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
... or, directly in your selector:
<YourSelector.ItemsPanel>
<ItemsPanelTemplate>
<blackjackandhookers:MyVirtualizingStackPanel/>
</ItemsPanelTemplate>
</YourSelector.ItemsPanel>
Enjoy!
I hope my answer will help you.

How to highlight one or more cells of a row on datagrid rowload in silverlight

I have the following code attached to the row load event on my datagrid :
private void myGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
MyObject o = e.Row.DataContext as MyObject;
string highlightColour = ...;
if (o.Source == "...")
e.Row.Background = ...;
else
e.Row.Background = null;
}
The issue is I am in need of highlighting some cells in the row and not others instead of the whole row, how do I go about acheiving that?
you can do it as below
Assume you need to change cell index 1 and 2
e.Row.Cells[1].Background = ....
e.Row.Cells[2].Background = ....
If you`re ok with XAML solution then you can use CellStyle for this:
<controls:DataGridTextColumn.CellStyle>
<Style TargetType="controls:DataGridCell">
<Setter Property="Background" Value="{Binding MyProperty,
Converter={StaticResource myConverter}}" />
</Style>
</controls:DataGridTextColumn.CellStyle>
Where MyProperty is your objects property you need to check and myConverter is a IValueConverter that returns some Brush based on your logic.
This example is just for one column but you can move this style to resources and use in every column that needs that functionality.

Listbox Not Tracking User Input

I've got a ListBox that is displaying a dynamic number of TextBoxes. The user will enter text into these boxes. When the Submit button is clicked, I need to be able to access the text the user has input, should be at ListBox.Items, like so:
//Called on Submit button click
private void SaveAndSubmit(object sender, ExecutedRoutedEventArgs e)
{
var bounds = MyListBox.Items;
}
But MyListBox.Items doesn't change after I initially set the ItemsSource, here:
//Field declaration
//Bounds is containing a group of strings that represent the boundaries
//for a contour plot. The min/max values are stored at the front and back
//of the group. However, there can be any number of dividers in between.
public ObservableCollection<string> Bounds { get; set; }
...
//Initialize Bounds in the constructor
//Called when the selected item for DVList (an unrelated ListBox) is changed
private void DVSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedDV = DVList.SelectedItem as DVWrapper;
if (selectedDV != null)
{
//Setting min/max
Bounds[0] = selectedDV.MinValue;
Bounds[Bounds.Count - 1] = selectedDV.MaxValue;
MyListBox.ItemsSource = Bounds;
}
}
My XAML looks like this:
<Window.Resources>
<Style x:Key="BoundsStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
...
<TextBox/>
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Focusable" Value="False"/>
</Style>
</Window.Resources>
...
<ListBox Name="MyListBox"
ItemContainerStyle="{StaticResource BoundsStyle}"/>
So when SaveAndSubmit is called, bounds ends up being what I had originally set it to in DVSelectionChanged. In other words, the listbox is not updating based on what the user has input into the textboxes contained in listbox. How can I get the updated ListBoxItems? I think my problem is similar to this one, but it's not working for me at the moment.
When I step through in the debugger, I can get individual ListBoxItems. However, their Content is empty. I'm looking into that right now.
You need to bind content of the textbox.
<TextBox/> need to change to <TextBox Content="{Binding}"/>
But follow MVVM else it will be difficult to find these errors.

ComboBox locks at application start

Ok, this is a strange one (or I'm doing something stupidly).
I have a WPF combobox which is populated with a string array on form load (app start).
This part works fine. The sticky bit is when I try to alter any of the information within said combobox. The debug says that it is changing but nothing is being shown visually.
// Populate the combobox:
private void ComboBlocks()
{
comboBox1.Items.Clear();
string[,] _tmp = _kits.BlockIDNames;
string[] _tmp1 = new string[_tmp.GetLength(0)];
for (int i = 0; i < _tmp.GetLength(0); i++)
{
_tmp1[i] = _tmp[i, 0] + " - " + _tmp[i, 1];
}
foreach (string s in _tmp1)
{
string[] _tmpS1 = s.Split(new char[] { '-' });
int _tmpS2 = Convert.ToInt32(_tmpS1[0].Trim());
bool _banneditem = _cbi.BannedItemExists(_tmpS2);
if (_banneditem == true)
AddComboItem(s, true);
else
AddComboItem(s);
}
if (comboBox1.Items.Count > 0)
comboBox1.SelectedIndex = 0;
}
// Add item to combobox:
private void AddComboItem(string _text,bool _redtext = false)
{
Grid grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
TextBlock text = new TextBlock();
text.Text = _text;
if (_redtext == true)
text.Foreground = Brushes.Red;
else
text.Foreground = Brushes.Black;
grid.Children.Add(text);
Grid.SetColumn(text, 0);
ComboBoxItem comboBoxItem = new ComboBoxItem();
comboBoxItem.Content = grid;
comboBoxItem.Tag = _text;
comboBox1.Items.Add(comboBoxItem);
}
Also, I am fairly new to C# so if there is anything that I'm doing wrong/inefficiently, please point it out.
Many thanks.
EDIT: Iterating through each one and changing the text value would probably be just as much work as it gets its information from another array. It would have to check for each item in the array and if it exists, colour it red, if it doesn't, colour it black.
I can't see what, specifically, you're doing wrong here. You can probably get that to work if you hack around on it enough.
But it's not the right approach - at least, not as far as writing maintainable, reliable WPF code is concerned. You should be using data binding to populate this control. You will find, after very little experience with doing this, that it makes for much faster, easier development than the code-WPF-as-if-it-were-WinForms approach you're taking.
Here's how:
Create a class for holding your data items, called, e.g. Block. Have it expose string Text and bool IsBanned properties.
Create an ObservableCollection<Block> and populate it with new Block objects created from your data source.
Expose the collection to binding. There are a lot of ways to do this; the example below assumes you've added it to the window's resource dictionary with a key of Blocks. You could also implement a Blocks property in your window, or (what I do pretty much any time I create a window or user control in WPF) create a class that exposes a Blocks property and set the window's DataContext to an instance of that class.
Now put this in your window's XAML:
<ComboBox ItemsSource="{Binding {DynamicResource Blocks}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Text}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsBanned}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
You don't actually need that Grid in there (nor do you need to set Grid.Column, since it defaults to 0); I just put that in there so that the example would more accurately replicate what's in your code. Also, I'm not setting the Tag property on the ComboBoxItem, but that's because the ComboBox's SelectedItem contains the actual Block instance for that item, which obviates the need for the Tag property.
Since you're using an ObservableCollection, any changes that you make to the collection (i.e. adding/removing/reordering its items) will automatically be reflected in what's on the screen; binding will take care of that for you.
If the items' Text or IsBanned properties will be changing after you populate the collection, and you need the ComboBox to reflect these changes, you'll need to implement INotifyPropertyChanged in the Block class and have it raise PropertyChanged in the setters of those properties.

Categories

Resources