How to manipulate individual group headers of gridview - c#

The following scenario:
I use a gridview to present grouped data.
I added a TextBlock to the headertemplate which
should contain the number of items in this group. (For example)
( Edit: In my scenario i show always 6 items and want to show the overflow in the TextBlock children of my HeaderTemplate )
How can i access the individual group headers from code to manipulate this TextBlock?
Here is an example of the result:
And here a simplified example of my GroupHeaderTemplate:
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock x:name="overflow"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
So i want to access and manipulate the "overflow" item individually for each generated group!

Here's what you really want.
First edit the GroupStyle HeaderTemplate
<GridView.GroupStyle>
<GroupStyle HidesIfEmpty="True">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Width="500" Margin="5,0,0,5">
<TextBlock HorizontalAlignment="Left">
<Run Text="{Binding Name}" />
<Run FontFamily="Segoe Ui Symbol" Text="" />
</TextBlock>
<TextBlock HorizontalAlignment="Right">
<Run Text="{Binding Children.Count, FallbackValue=0}" />
<Run Text="Items" />
</TextBlock>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid />
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</GridView.GroupStyle>
please note I am using a VaraibleSizedWrapGrid
Next, handle the GridView's Loaded Event
class SubtractConverter : IValueConverter
{
public double Amount { get; set; }
public object Convert(object v, Type t, object p, string l)
{ return System.Convert.ToDouble(v) - Amount; }
public object ConvertBack(object v, Type t, object p, string l)
{ throw new NotImplementedException(); }
}
private void GridView_OnLayoutUpdated(object sender, RoutedEventArgs e)
{
var grid = sender as GridView;
var converter = new SubtractConverter { Amount = 5 * 2 /* padding x2 */ };
foreach (GroupItem group in (grid.ItemsPanelRoot as Panel).Children)
{
var result = VisualTreeHelper.GetChild(group, 0);
while (!(result is Grid))
result = VisualTreeHelper.GetChild(result, 0);
var items = (result as Panel).Children.OfType<ItemsControl>()
.First().ItemsPanelRoot;
var binding = new Binding
{
Path = new PropertyPath("ActualWidth"),
Mode = BindingMode.OneWay,
Converter = converter,
Source = items,
};
var header = (result as Panel).Children.OfType<ContentControl>()
.First().ContentTemplateRoot as FrameworkElement;
header.SetBinding(FrameworkElement.WidthProperty, binding);
}
}
And, presto! Now your header is perfectly sized to the width of the items in the group.
Things to remember (as the designer):
Your grouped items might be as narrow as a single column. Solve this with TextTrimming in the Header TextBoxes.
Your grouped items might be wider than the monitor and # items might be off-screen. Solve this with a MinWidth on the containing grid.
Best of luck!

I finally managed to get my hands on the desired Element by using the VisualTreeHelperExtensions
First of all you have to install XamlToolkit via Nuget
Afterwards add a using directive for the Extensions:
using WinRTXamlToolkit.Controls.Extensions;
Now you can use several more methods on your ui elements , one of them is getDescendantsByType()
which i use to get all textblock elements hold by my gridview. I added a tag to my overflow textblocks which i check for when iterating through the gridviews descendants, see for yourself:
private void ItemsGridView_Loaded(object sender, RoutedEventArgs e)
{
foreach (TextBlock element in this.myGridView.GetDescendantsOfType<TextBlock>())
{
if(element.Tag != null && element.Tag.Equals("itemCountBlock")){
element.Text = "Finally solved!";
}
}
}
This should work out for any ui element and any property one would wanna change.

Related

Code-behind doesn't recognize the name of label

I created a textblock on a XAML form within a ContentControl. When I try to program it, C# doesn't recognize the name and I can't do anything with it.
I tried adding a textblock to the form outside of the Content Control, but that still didn't fix the problem.
Here is the XAML code:
<ContentControl>
<ContentControl.Template>
<ControlTemplate>
<Grid VerticalAlignment="Bottom" Height="250" Margin="0,450,0,0">
<Rectangle Fill="Beige" Stroke="Black" StrokeThickness="3"
Width="639" Height="250" Margin="0,0,0,0"/>
<TextBlock Text="Goal:" FontSize="18" Margin="7,50,0,0"/>
<TextBlock Text="Eaten:" FontSize="18" Margin="7,120,0,0"/>
<TextBlock Text="Remaining:" FontSize="18" Margin="7,190,0,0"/>
<TextBlock Text="Calories:" FontSize="18" Margin="140,10,0,0"/>
<TextBlock Text="Fat(g):" FontSize="18" Margin="270,10,0,0"/>
<TextBlock Text="Carbs(g):" FontSize="18" Margin="380,10,0,0"/>
<TextBlock Text="Protein(g):" FontSize="18" Margin="520,10,0,0"/>
<TextBlock x:Name="lblCalorieGoal" Text="Peb"
TextAlignment="Center" FontSize="18" Margin="-290,50,0,0"/>
</Grid>
</ControlTemplate>
</ContentControl.Template>
<TextBlock Text="TextBlock" TextWrapping="Wrap"/>
</ContentControl>
And then here is the corresponding working C# code:
public LogFood()
{
this.InitializeComponent();
Windows.Storage.ApplicationDataContainer localSettings =
Windows.Storage.ApplicationData.Current.LocalSettings;
Windows.Storage.StorageFolder localFolder =
Windows.Storage.ApplicationData.Current.LocalFolder;
Windows.Storage.ApplicationDataCompositeValue composite =
(Windows.Storage.ApplicationDataCompositeValue)localSettings
.Values["nutritionSettings"];
int calorieMin = Convert.ToInt32(composite["calorieMin"]);
int calorieMax = Convert.ToInt32(composite["calorieMax"]);
int gramsFatMin = Convert.ToInt32(composite["gramsFatMin"]);
int gramsFatMax = Convert.ToInt32(composite["gramsFatMax"]);
int gramsCarbsMin = Convert.ToInt32(composite["gramsCarbsMin"]);
int gramsCarbsMax = Convert.ToInt32(composite["gramsCarbsMax"]);
int gramsProteinMin = Convert.ToInt32(composite["gramsProteinMin"]);
int gramsProteinMax = Convert.ToInt32(composite["gramsProteinMax"]);
lblCalorieGoal.Text = calorieMin;
}
I expect to be able to change the text of the textblock. Instead, I get the error, "The name lblCalorieGoal.Text does not exist in the current context."
The key realization here is that a template is potentially a reusable part of XAML, so anything inside is in fact embedded in it a not "publicly" accessible, as there could potentially be multiple instances of the same template materialized on the view.
That being said, you can still access the materialized children inside the template indirectly by searching for them within the template using VisualTreeHelper -
internal static FrameworkElement FindChildByName(DependencyObject startNode, string name)
{
int count = VisualTreeHelper.GetChildrenCount(startNode);
for (int i = 0; i < count; i++)
{
DependencyObject current = VisualTreeHelper.GetChild(startNode, i);
if (current is FrameworkElement frameworkElement)
{
if (frameworkElement.Name == name)
return frameworkElement;
}
var result = FindChildByName(current, name);
if ( result != null)
{
return result;
}
}
return null;
}
Note, that this works only after the control has loaded (for example in the Page.Loaded event handler -
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var block = FindChildByName(ContentRoot, "lblCalorieGoal") as TextBlock;
}
However, this all is not an ideal solution to your problem. Instead, you should either ditch the use of ContentControl altogether and have the controls in the template directly on the page (which would make them directly accessible from the code-behind), or/and use data-binding to bind data directly to appropriate controls. In this case, I would create a class to hold the data, for example:
public class NutritionInfo
{
public string CalorieGoal { get; set; }
}
Now instead of ContentControl.ControlTemplate (which replaces the template of the whole control), you will replace the ContentTemplate instead (which is just the thing which `ControlTemplate in fact displays):
<ContentControl x:Name="ContentRoot">
<ContentControl.ContentTemplate>
<DataTemplate x:DataType="local:NutritionInfo">
... your template
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
Note we use x:DataType to specify the type we bind to so that we can use x:Bind syntax. Finally, we update the template itself:
<TextBlock x:Name="lblCalorieGoal" Text="{x:Bind CalorieGoal}" ... />
We use x:Bind to bind the text of the TextBlock to the CalorieGoal property. We are almost done, now just set the Content property of the ContentControl to an instance of NutritionInfo (for example via data binding or directly):
ContentRoot.Content = new NutritionInfo()
{
CalorieGoal = "1243"
};
Overall I recommend to read further about how data-binding works in XAML, as that will help you significantly simplify your code and avoid accessing controls directly via x:Name, and decouple UI from your code. See documentation for more info.

Center aligning GridView items in last row

I want to implement a GridView which takes 3 items in a row, and if the number of items are 2 in last row, then the last row items should be aligned center instead of being left-aligned. Here are a couple of images to explain what I want to achieve.
Currently my implementation looks like
.
And this is what I want to achieve.
Any help would be appreciated.
There are many ways realizing the feature that you mentioned.
To summarize it, you need to inherit GridView and override MeasureOverride ArrangeOverride method to re-calculate each Rect of Panel's children. This way is complex. For more info you could refer to
XAML custom panels overview.
And you could also use PrepareContainerForItemOverride method to re-layout the item directly.
<local:VariableGrid
x:Name="MyGridView"
SelectionMode="Single"
IsSwipeEnabled="False">
<local:VariableGrid.ItemTemplate >
<DataTemplate>
<StackPanel BorderBrush="Red" BorderThickness="3" Height="200" Width="200" Margin="20">
</StackPanel>
</DataTemplate>
</local:VariableGrid.ItemTemplate>
<local:VariableGrid.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid
Orientation="Horizontal"
VerticalAlignment="Top"
ScrollViewer.HorizontalScrollMode="Enabled"
ScrollViewer.VerticalScrollMode="Disabled"
MaximumRowsOrColumns="4">
</VariableSizedWrapGrid>
</ItemsPanelTemplate>
</local:VariableGrid.ItemsPanel>
</local:VariableGrid>
VariableGrid.cs
public sealed class VariableGrid : GridView
{
public VariableGrid()
{
this.DefaultStyleKey = typeof(VariableGrid);
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
var list = this.ItemsSource as List<string>;
var griditem = element as GridViewItem;
for (var t = ((list.Count - list.Count % 4)); t < list.Count; t++)
{
if (item as string == list[t])
{
if (griditem != null)
{
VariableSizedWrapGrid.SetColumnSpan(griditem, 2);
}
}
}
base.PrepareContainerForItemOverride(element, item);
}
}
However, this simple way can not fit all the scenario.

Modify the visibility of a row via keyboard interaction in UWP application

Here is my scenario. My table has fixed number of columns, say 2, and initially, it has only one visible row, but when the focus is on the last column of row 1 and the user press 'tab', row 2 will be made visible.
My problem is that I can't dynamically select the row I want to make visible because I have to specify its x:Name during compilation.
Below is my current work.
.xaml file
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" x:Name="SP1">
<TextBox Text="1-1"/>
<TextBox Text="1-2" KeyDown="showNextLine"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="SP2" Visibility="Collapsed">
<TextBox Text="2-1"/>
<TextBox Text="2-2"/>
</StackPanel>
<!--the remaining rows...-->
</StackPanel>
.cs file
private int lastRowIndex = 1;
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
string nextLineName = "SP" + lastRowIndex.ToString();
nextLineName.Visibility = Visibility.Visible; // which causes error because nextLineName is string instead of StackPanel
}
Besides, my current implementation is to create 50 rows and make the last 49 invisible initially, and I am open to any method to group all the TextBox more systematically or flexibly.
Thanks for reading.
You could give the parent StackPanel an x:Name or keep a reference to it if you create it dynamically:
<StackPanel x:Name="root" Orientation="Vertical">
<StackPanel Orientation="Horizontal" x:Name="SP1">
<TextBox Text="1-1"/>
<TextBox Text="1-2" KeyDown="showNextLine"/>
</StackPanel>
<StackPanel Orientation="Horizontal" x:Name="SP2" Visibility="Collapsed">
<TextBox Text="2-1"/>
<TextBox Text="2-2"/>
</StackPanel>
<!--the remaining rows...-->
</StackPanel>
...and then get a reference to a child StackPanel using the Children property and some basic LINQ:
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
string nextLineName = "SP" + lastRowIndex.ToString();
StackPanel child = root.Children.OfType<StackPanel>().FirstOrDefault(x => x.Name == nextLineName);
if (child != null)
child.Visibility = Visibility.Visible;
}
How could I create a StackPanel dynamically?
Like this:
var sp = new StackPanel { Name = "SP3", Orientation = Orientation.Horizontal, Visibility = Visibility.Collapsed };
sp.Children.Add(new TextBlock { Text = "3-1" });
var txt = new TextBlock() { Text = "3-2" };
txt.KeyDown += showNextLine;
sp.Children.Add(txt);
root.Children.Add(sp);
I can't think of an easy way to do the first part of this (since you have 50 stack panels), but if you put all of them in a dictionary, then you could update them using just the key.
Here's the dictionary part done manually:
Dictionary<int, StackPanel> myStackPanels = new Dictionary<int, StackPanel>();
myStackPanels.Add(1, SP1);
myStackPanels.Add(2, SP2);
Then, here's what ShowNextLine would look like:
private void showNextLine(object sender, KeyRoutedEventArgs e)
{
lastRowIndex++;
// Modify the StackPanel whose key is lastRowIndex;
myStackPanels[lastRowIndex] = Visibility.Visible;
}

WPF - Elements inside DataTemplate property issue when no binding?

I have the following TabControl:
<TabControl ItemsSource="{Binding Tabs"}>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type vm:TabVM}">
<TextBox></TextBox>
<TextBox Text="{Binding SomeProperty}"></TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
The unexpected behaviour is that first TextBox has Text property shared between all tabitems, while second TextBox effectively bind to ViewModel property.
My need is to make independent the first TextBox too, even without binding.
What can I do ?
** UPDATE **
After several tries I've decided to use the ikriv's TabContent.cs.
The only issue I've found with this is that calling the TabControl.Items.Refresh() (i.e. after removing a tabItem) cause the reset of the internal cache.
An unelegant but effective solution may be this:
public ContentManager(TabControl tabControl, Decorator border)
{
_tabControl = tabControl;
_border = border;
_tabControl.SelectionChanged += (sender, args) => { UpdateSelectedTab(); };
/* CUSTOM */
var view = CollectionViewSource.GetDefaultView(((TabControl)_tabControl).Items);
view.CollectionChanged += View_CollectionChanged;
}
/*
* This fix the internal cache content when calling items->Refresh() method
* */
private void View_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
/* Retrieve all tabitems cache and store to a temp list */
IList<ContentControl> cachedContents = new List<ContentControl>();
foreach (var item in _tabControl.Items)
{
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
var cachedContent = TabContent.GetInternalCachedContent(tabItem);
cachedContents.Add(cachedContent);
}
/* rebuild the view */
_tabControl.Items.Refresh();
/* Retrieve all cached content and store to the tabitems */
int idx = 0;
foreach (var item in _tabControl.Items)
{
var tabItem = _tabControl.ItemContainerGenerator.ContainerFromItem(item);
TabContent.SetInternalCachedContent(tabItem, cachedContents[idx++]);
}
}
}
You should use data binding since the same ContentTemplate will be applied for all items in your ItemsSource. Only the binding will be refreshed when you switch tabs basically. The TextBox isn't re-created nor reset.
What can I do ?
You could work around this in the view by handling the SelectionChanged event of the TabControl and reset the TextBox control yourself:
private void tabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
TabControl tc = sender as TabControl;
ContentPresenter cp = tc.Template.FindName("PART_SelectedContentHost", tc) as ContentPresenter;
if(cp != null && VisualTreeHelper.GetChildrenCount(cp) > 0)
{
ContentPresenter cpp = VisualTreeHelper.GetChild(cp, 0) as ContentPresenter;
if(cpp != null)
{
TextBox textBox = cpp.FindName("txt") as TextBox;
if (textBox != null)
textBox.Text = string.Empty;
}
}
}
<TabControl x:Name="tabs" ItemsSource="{Binding Tabs}" SelectionChanged="tabs_SelectionChanged">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter>
<ContentPresenter.Content>
<StackPanel>
<TextBox x:Name="txt"></TextBox>
</StackPanel>
</ContentPresenter.Content>
</ContentPresenter>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
If you want to persist the text in the TextBox when you switch tabs you could use the attached behaviour from the following article and set its IsCached property to true: https://www.codeproject.com/articles/460989/wpf-tabcontrol-turning-off-tab-virtualization
<TabControl ItemsSource="{Binding Items}" behaviors:TabContent.IsCached="True">
<!-- Make sure that you don't set the TabControl's ContentTemplate property but the custom one here-->
<behaviors:TabContent.Template>
<DataTemplate>
<StackPanel>
<TextBox />
</StackPanel>
</DataTemplate>
</behaviors:TabContent.Template>
</TabControl>
Yet another approach would be to modify the ControlTemplate of the TabControl to include a ListBox as suggested by 'gekka' in the following thread on the MSDN forums: https://social.msdn.microsoft.com/Forums/en-US/4b71a43a-26f5-4fef-8dc5-55409262298e/using-uielements-on-datatemplate?forum=wpf

binding data - inotifypropertychanged does not work

I have a listBox1 in which data are binding from the list. Then I want to when I select any item from listBox1 in listBox2 will binding data from another list.
private void listBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Teams teams = (Teams)listBox1.SelectedItems[0];
getH2hResults("//td[#class='hell']", teams.Team1, teams.Team2); // add elements to list
getH2hResults("//td[#class='dunkel']", teams.Team1, teams.Team2); // and here also
listBox2.ItemsSource = lists.h2hList;
}
On the first time this work, but for the twice time listBox2 doesn't displays new data.
public class Lists : BindableBase
{
public Lists()
{
_teamsList = new List<Teams>();
_h2hList = new List<H2H>();
}
private List<Teams> _teamsList;
public List<Teams> teamsList
{
get
{
return _teamsList;
}
set
{
if (value != _teamsList)
{
_teamsList = value;
RaisePropertyChanged("teamsList");
}
}
}
private List<H2H> _h2hList;
public List<H2H> h2hList
{
get
{
return _h2hList;
}
set
{
if (value != _h2hList)
{
_h2hList = value;
RaisePropertyChanged("h2hList");
}
}
}
}
And XAML
<ListBox Name="listBox1" Width="300" Height="300"
VerticalAlignment="Top"
HorizontalAlignment="Left"
ItemsSource="{Binding teamsList}" SelectionChanged="listBox1_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Foreground="#FF4273CD" Text="{Binding Team1, Mode=TwoWay}"></TextBlock>
<TextBlock Text=" vs " FontWeight="Bold"></TextBlock>
<TextBlock Foreground="#FF4273CD" Text="{Binding Team2, Mode=TwoWay}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ListBox Name="listBox2" Grid.Column="1" Width="300" Height="300"
VerticalAlignment="Top"
HorizontalAlignment="Left"
ItemsSource="{Binding h2hList}" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding date, Mode=TwoWay}"></TextBlock>
<TextBlock Text="{Binding result, Mode=TwoWay}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
RaisePropertyChanged("teamList");
is Wrong your propery is named 'teamsList' with an S,
change to:
RaisePropertyChanged("teamsList");
It is the public property you bind to and notify changes of,
edit:
also change your binding:
ItemsSource="{Binding teamList}"
to
ItemsSource="{Binding teamsList}"
Edit 2:
listBox2.DataContext = xxx
Not itemsource = xxx
With the line (in listBox1_SelectionChanged)
listBox2.ItemsSource = lists.h2hList;
you are effectively removing the binding from the ItemsSource property of listBox2.
Instead, you should only update the h2hList property in your Lists class (which presumably happens in getH2hResults) and remove the above line from your code.
Note however that it is not sufficient to clear and re-fill that list. You need to set the h2hList property in order to get a property change notification raised:
var newList = new List<H2H>();
// fill newList before assigning to h2hList property
lists.h2hList = newList;
If you want to keep the list and just change its elements, you would need to use ObservableCollection<H2H> instead of List<H2H> as collection type. This would be the better approach anyway, as you would not have to care for when exactly you add elements to a newly created collection.

Categories

Resources