How do you put an ObservableCollection into a Textblock - c#

I need to put my ObservableCollection<ValidationMessage> into my TextBlock. Here is my code. Right now it is showing the Item and the SubItems, but where the messages show it has System.Collections.ObjectModel.ObservableCollection'1[ValidationWPF.DataSources.‌​ValidationMessages].
I think this is because it cannot put an ObservableCollection into the TextBlock.
XAML:
<UserControl x:Class="ValidationWPF.ValidationUserControl"
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"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:local="clr-namespace:ValidationWPF.DataSources"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate x:Key="Messages">
<TextBlock Text="{Binding Message}"/>
</DataTemplate>
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<telerik:RadTreeView x:Name="radTreeView" Margin="8">
<telerik:RadTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SubItems}">
<TextBlock Text="{Binding item}" />
</HierarchicalDataTemplate>
</telerik:RadTreeView.ItemTemplate>
</telerik:RadTreeView>
</Grid>
</UserControl>
ValidationMessage Class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ValidationWPF.DataSources
{
public class ValidationMessage
{
public ValidationMessage(string Message)
{
this.Message = Message;
}
public string Message
{
get;
set;
}
}
}
ValidationItem Class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
namespace ValidationWPF.DataSources
{
public class ValidationItem : ObservableCollection<ValidationItem>
{
public ValidationItem()
{
SubItems = new ObservableCollection<ValidationItem>();
}
public ObservableCollection<ValidationMessage> Message
{
get;
set;
}
public string item
{
get;
set;
}
public IList<ValidationItem> SubItems
{
get;
set;
}
public static IList<ValidationItem> GetItems(string name)
{
var Validation = new ObservableCollection<ValidationItem>();
var item = new ValidationItem();
item.item = "Customer";
var subItem = new ValidationItem();
subItem.item = "Name";
item.SubItems.Add(subItem);
var Message = new ValidationItem();
Message.item = new ObservableCollection<ValidationMessage>().ToString();
subItem.SubItems.Add(Message);
Validation.Add(item);
return Validation;
}
}
}
Thank you for your help!!

The problem is that the Text property of the TextBlock is a string, and you're giving it an ObservableCollection. The only way WPF knows to convert the two is by calling ObservableCollection.ToString(), which returns the full type name of the class.
The fix is to convert your ObservableCollection into a string by creating a class that implements System.Windows.Data.IValueConverter. This allows you to control the conversion.
You could implement it something like this:
using System.Globalization;
using System.Text;
using System.Windows.Data;
namespace ValidationWPF.DataSources
{
class CollectionConverter : IValueConverter
{
object Convert(object value, Type targetType,object parameter,CultureInfo culture)
{
ObservableCollection<ValidationMessage> messages = (ObservableCollection<ValidationMessage>)value;
var sb = new StringBuilder();
foreach(var msg in messages)
{
sb.AppendLine(msg.Message);
}
return sb.ToString();
}
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}
And you can use it like this in your XAML file:
<UserControl x:Class="ValidationWPF.ValidationUserControl"
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"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:local="clr-namespace:ValidationWPF.DataSources"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<local:CollectionConverter x:Key="CollectionConverter" />
<DataTemplate x:Key="Messages">
<TextBlock Text="{Binding Message, Converter={StaticResource CollectionConverter}}"/>
</DataTemplate>
</UserControl.Resources>
...
</UserControl>
Now WPF will call CollectionConverter.Convert() whenever it needs to populate your TextBlock.

You are right. The TextBlock tries to view the property value as a String and an ObservableCollection.ToString will return just what you saw.
What you could do is to add a new property that combines all the messages of the ObservableCollection into a single string. Something like this:
public string MessagesCombined
{
get { return string.Join(Environment.NewLine, Message.Select(m => m.Message)); }
}
This will combine all the Messages in your ObservableCollection into a single string, with each item separated by a newline. (You may have to modify my code somewhat, I am writing this without access to a compiler...).

I ended up doing it a different and cleaner way.
XAML:
<UserControl x:Class="ValidationWPF.ValidationUserControl"
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"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:local="clr-namespace:ValidationWPF.DataSources"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<!--<local:CollectionConverter x:Key="CollectionConverter"/>
<DataTemplate x:Key="Messages">
<TextBlock Text="{Binding Message}"/>
</DataTemplate>-->
</UserControl.Resources>
<Grid x:Name="LayoutRoot" Background="White">
<telerik:RadTreeView x:Name="radTreeView" Margin="8" ItemsSource="{Binding Errors}">
<telerik:RadTreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding SubItems}" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding Description}"/>
<ListBox Grid.Row="1" ItemsSource="{Binding Messages}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Message}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</HierarchicalDataTemplate>
</telerik:RadTreeView.ItemTemplate>
</telerik:RadTreeView>
</Grid>
</UserControl>
VALIDATIONMESSAGE CLASS:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ValidationWPF.DataSources
{
public class ValidationMessage
{
public ValidationMessage(string name, string Message)
{
this.Message = Message;
this.PropertyName = name;
}
public string Message
{
get;
set;
}
public string PropertyName { get; set; }
}
}
VALIDATIONVIEWMODEL CLASS:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
namespace ValidationWPF.DataSources
{
public class ValidationViewModel
{
public ValidationViewModel()
{
this.Errors = new ObservableCollection<ValidationItem>();
ValidationItem item = new ValidationItem();
item.Description = "Customer";
ValidationMessage msg = new ValidationMessage("FirstName", "First name is required");
item.Messages.Add(msg);
this.Errors.Add(item);
ValidationItem item2 = new ValidationItem();
item2.Description = "Order";
msg = new ValidationMessage("Quantity", "Quantity must be greater than zero");
item2.Messages.Add(msg);
item.SubItems.Add(item2);
}
public ObservableCollection<ValidationItem> Errors { get; set; }
}
}
VALIDATIONUSERCONTROL CLASS:
public partial class ValidationUserControl : UserControl
{
public ValidationUserControl()
{
InitializeComponent();
this.DataContext = new ValidationViewModel();
}
}

Related

WPF TwoWay Binding not work with CustomControl in Template

I have some problems with my Custom Control - Two way binding don't work when I use it in template.
So I have created template xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
xmlns:controls="clr-namespace:GUIControls;assembly=GUIControls"
>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1"
xmlns:controls="clr-namespace:GUIControls;assembly=GUIControls"
>
<ControlTemplate x:Key="YParamCombo" TargetType="ContentControl">
<controls:ParamCombo Header="MY CONTROL TEMPLATE"
Items="{Binding Items}"
PCValue="{Binding Codes[MY_CONTROL_TEMPLATE], Mode=TwoWay}"
Required="True"
MultiSelect="False"/>
</ControlTemplate>
<ControlTemplate x:Key="YComboBox" TargetType="ContentControl">
<ComboBox DisplayMemberPath="Name"
StaysOpenOnEdit="True"
ItemsSource="{Binding Items}"
SelectedValue="{Binding Codes[STANDARD_TEMPLATE], Mode=TwoWay}"
SelectedValuePath="Code"/>
</ControlTemplate>
MainWindow.xaml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
xmlns:controls="clr-namespace:GUIControls;assembly=GUIControls"
Title="MainWindow" Height="250" Width="525">
<Grid Margin="0,0,0,-1">
<Button Margin="62,162,299,4" Content="Show Codes-1" Click="Button_Click2"></Button>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<HeaderedContentControl Header="STANDARD CONTROL XAML" >
<ComboBox DisplayMemberPath="Name"
StaysOpenOnEdit="True"
ItemsSource="{Binding Items}"
SelectedValue="{Binding Codes[STANDARD_XAML]}"
SelectedValuePath="Code"/>
</HeaderedContentControl>
<HeaderedContentControl Header="STANDARD CONTROL TEMPLATE" >
<ContentControl Height="23" Template="{StaticResource YComboBox}"/>
</HeaderedContentControl>
<ContentControl Height="44" Template="{StaticResource YParamCombo}">
</ContentControl>
<controls:ParamCombo Header="MY CONTROL XAML"
Items="{Binding Items}"
PCValue="{Binding Codes[MYCONTROL_XAML], Mode=TwoWay}"
Required="True"
MultiSelect="False"/>
</StackPanel>
</Grid>
cs
using System.Linq;
using System.Windows;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
DataContext = new WModel();
InitializeComponent();
}
private WModel vm { get { return (DataContext as WModel); } }
private void Button_Click2(object sender, RoutedEventArgs e)
{
MessageBox.Show(string.Join(";", vm.Codes._codes.Select(x => x.Key + "=" + x.Value).ToArray()));
}
}
}
using GUIControls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace WpfApp1
{
public class WModel
{
public WModel()
{
Codes = new CodesClass();
}
public string Caption { get; set; }
public ObservableCollection<Dict> Items
{
get
{
return new ObservableCollection<Dict>()
{
new Dict(){ Name = "Name1", Code = "Code1" } ,
new Dict(){ Name = "Name2", Code = "Code2" }
};
}
}
public CodesClass Codes { get; set; }
}
public class Dict : IDict
{
public string Name { get; set; }
public string Code { get; set; }
}
public class CodesClass
{
public Dictionary<string, object> _codes;
public CodesClass()
{
_codes = new Dictionary<string, object>();
}
public object this[string param]
{
get
{
if (_codes.ContainsKey(param))
return _codes[param];
else
return null;// "I have no " + param;
}
set
{
_codes[param] = value;
}
}
}
}
When I run app and select all 4 comboboxes and Press button, I can see that twoway binding in one combobox(Custom Control declared in template ) do not work
---------------------------
---------------------------
STANDARD_XAML=Code2;STANDARD_TEMPLATE=Code2;MYCONTROL_XAML=Code2
---------------------------
ОК
---------------------------
Some code from control
public static readonly DependencyProperty PCValueProperty =
DependencyProperty.Register("PCValue", typeof(string), typeof(ParamCombo),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnPCValuePropertyChanged)));
//new PropertyMetadata(new PropertyChangedCallback(OnValuePropertyChanged)));, new PropertyChangedCallback(OnPCValuePropertyChanged))
#endregion
private static void OnPCValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
ParamCombo paramCombo = (ParamCombo)sender;
paramCombo.UpdateSelected((e.NewValue == null) ? "" : e.NewValue.ToString());
}
Thanks for your help!
I have had problems with twoway binding in a combo in a customcontrol template and the solution was to override OnApplyTemplate in the custom control, using Template.FindName to get the combo, get the DataContex object of the combo and raise PropertyChanged in the DataContext object for the bound property. My problem was to update the combo when the window was loaded.

ListBox not refreshing using mvvm

I have asked how to bind listbox to mvvm in this thread:
Listbox is not Populating using binding
I created another thread to ask why my ObservableList is not Refreshing.
The List is now populated on Load
but not populating or refreshing after I select the Folder
The program is supposed to Populate the list after I select the program
this is the structure of my folders and class
this is the code for my FolderBrowserDialogVM
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Bates_Writer.ViewModel
{
public class FolderBrowserDialogVM :ViewModelBase
{
public System.Collections.ObjectModel.ObservableCollection<string> FileNames { get; }
= new System.Collections.ObjectModel.ObservableCollection<string>()
{
"Wayne",
"Cinderella",
"Malificient"
};
public static RelayCommand OpenCommand { get; set; }
private string _selectedPath;
public string SelectedPath
{
get { return _selectedPath; }
set
{
_selectedPath = value;
RaisePropertyChanged("SelectedPath");
}
}
private string _defaultPath;
public FolderBrowserDialogVM()
{
RegisterCommands();
}
public FolderBrowserDialogVM(string defaultPath)
{
_defaultPath = defaultPath;
RegisterCommands();
}
private void RegisterCommands()
{
OpenCommand = new RelayCommand(ExecuteOpenFileDialog);
}
private void ExecuteOpenFileDialog()
{
var dialog = new FolderBrowserDialog();
dialog.ShowDialog();
SelectedPath = dialog.SelectedPath;
FileNames.Clear();
foreach (var file in System.IO.Directory.EnumerateFiles(SelectedPath, "*", System.IO.SearchOption.AllDirectories))
{
FileNames.Add(file);
Console.WriteLine(file);
}
}
}
}
this is the code for FilesView.xaml(user control)
<UserControl x:Class="Bates_Writer.View.FilesView"
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"
xmlns:local="clr-namespace:Bates_Writer.View"
xmlns:vm="clr-namespace:Bates_Writer.ViewModel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<vm:FolderBrowserDialogVM>
</vm:FolderBrowserDialogVM>
</UserControl.DataContext>
<Grid>
<ListBox ItemsSource="{Binding FileNames}" Margin="5,5,5,5"/>
</Grid>
</UserControl>
I tried How to: Implement Property Change Notification
but I can't make it work.
this is my FolderBrowserDialogV.xaml(user control)
<UserControl x:Class="Bates_Writer.View.FolderBrowserDialogV"
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"
xmlns:local="clr-namespace:Bates_Writer.View"
xmlns:vm="clr-namespace:Bates_Writer.ViewModel"
mc:Ignorable="d" Height="42.056" Width="679.439">
<UserControl.DataContext>
<vm:FolderBrowserDialogVM>
</vm:FolderBrowserDialogVM>
</UserControl.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="127*"/>
<ColumnDefinition Width="506*"/>
<ColumnDefinition Width="110*"/>
</Grid.ColumnDefinitions>
<Label Content="Folder Location: " FontSize="14" VerticalContentAlignment="Center"/>
<TextBox Grid.Column="1" FontSize="14" Margin="5,5,5,5" VerticalContentAlignment="Center" Text="{Binding SelectedPath}"/>
<Button Command="{Binding OpenCommand}" Content="Browse" Grid.Column="2" FontSize="14" Margin="5,5,5,5" Cursor="Hand"/>
</Grid>
</UserControl>
I even uploaded the sample project here
It looks like you have two instances of FolderBrowserDialogVM. One instance is DataContext for your FolderBrowserDialogV(here you have bound the command, but not the ObservableCollection) and another one for your FilesView(here you have bound ObservableCollection, but not the command, so your ObservableCollection will not be changed in this instance). For it works in your case, you should use(bind to) the same instance.
Delete from your UserControls:
<UserControl.DataContext>
<vm:FolderBrowserDialogVM>
</vm:FolderBrowserDialogVM>
</UserControl.DataContext>
Create an instance of your ViewModel in resources of your main window and set this instance as DataContext for your Usercontrols:
<MainWindow
xmlns:vm="clr-namespace:Bates_Writer.ViewModel" .. >
<MainWindow.Resources>
<vm:FolderBrowserDialogVM x:Key="vmInstance"/>
</MainWindow.Resources>
<StackPanel>
<YourUserControl1 DataContext="{StaticResource vmInstance}"/>
<YourUserControl2 DataContext="{StaticResource vmInstance}"/>
</StackPanel>
</MainWindow>

Getting error with the x:datatype in xaml

My xaml code
<Page x:Class="WindowsApp2.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Behaviors="using:Template10.Behaviors"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:controls="using:Template10.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:WindowsApp2.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:WindowsApp2.ViewModels" mc:Ignorable="d">
<Page.DataContext>
<vm:MainPageViewModel x:Name="ViewModel" />
</Page.DataContext>
<RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ItemsControl ItemsSource="{x:Bind ViewModel.Detail,Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate
x:DataType="vm:Details">
<TextBox Margin="200,200,0,0"
Text="{x:Bind name,Mode=TwoWay}"></TextBox>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</RelativePanel>
My MainPageViewModel.cs
using Template10.Mvvm;
using System.Collections.Generic;
using System;
using System.Linq;
using System.Threading.Tasks;
using Template10.Services.NavigationService;
using Windows.UI.Xaml.Navigation;
using WindowsApp2.Views;
namespace WindowsApp2.ViewModels
{
public class Detail
{
public string name { get; set; }
public string pass { get; set; }
}
public class MainPageViewModel : ViewModelBase
{
public MainPageViewModel()
{
if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
{
Value = "Designtime value";
}
Details = new List<Detail>();
for(int i=0;i<5;i++)
{
Detail d = new Detail();
d.name = "kalp"+i.ToString();
d.pass = "1234"+i.ToString();
Details.Add(d);
}
}
public List<Detail> Details { get; set; }
}
}
Now My problem is that i am getting the error "The name "Detail" does not exist in the namespace "using:WindowsApp2.ViewModels". My Detail class is defined in the namespace WindowsApp2.ViewModels . So what is the problem here.
NOTE:I deleted some code that is not related to this error
The error you are describing is a side effect of an invalid binding that can't be compiled.
You are trying to bind to a view model property Detail, which indeed does not exist. What you certainly want is to bind to the Details property instead.
So replace
<ItemsControl ItemsSource="{x:Bind ViewModel.Detail, Mode=TwoWay}">
with
<ItemsControl ItemsSource="{x:Bind ViewModel.Details, Mode=OneWay}">
This does of course also require that there is a
public MainPageViewModel ViewModel { get; }
property in your MainPage class.
Note also that a TwoWay binding on the ItemsSource property doesn't make sense. It should instead be OneWay, or probably the default OneTime.

How to implement summary rows(Total rows) in WPF data-grid via MVVM

I need a help to to make a summary row or total a row in WPF datagrid using MVVM pattern, special of this summary row is having a value for each column like a image shows below.First total calculation base on first 3 items and those are in one group.I couldn't find a good example or sample code for this issue.
Please refer this image:
you can add a footer row to your datagrid like This or like This
-- updated --
if you need row Grouping have a look Here.. try to understand the concept of grouping and adding row group headers
Several solutions are possible. There is not enough detail in your explanations to select the most appropriate option.
Below is an example using the converter for summarizing by group and styling the group in the DataGrid.
namespace TotalRows
{
public class ItemClass
{
public int Group { get; set; }
public string Title { get; set; }
public int Y2013 { get; set; }
public int Y2014 { get; set; }
public int Y2015 { get; set; }
public int Y2016 { get; set; }
}
}
using System.Collections.ObjectModel;
using System.Linq;
namespace TotalRows
{
public class ExampleData
{
public static ObservableCollection<ItemClass> Items { get; }
= new ObservableCollection<ItemClass>()
{
new ItemClass() {Group=1, Title="Item1", Y2013=1200, Y2014=1500, Y2015=1800, Y2016=1500},
new ItemClass() {Group=1, Title="Item2", Y2013=2350, Y2014=2000, Y2015=2400, Y2016=2300},
new ItemClass() {Group=1, Title="Item3", Y2013=4000, Y2014=4350, Y2015=5000, Y2016=5500},
new ItemClass() {Group=2, Title="Item1", Y2013=1250, Y2014=1400, Y2015=1900, Y2016=1500},
new ItemClass() {Group=2, Title="Item2", Y2013=1350, Y2014=2500, Y2015=2450, Y2016=2700},
new ItemClass() {Group=2, Title="Item3", Y2013=3500, Y2014=3350, Y2015=5000, Y2016=5500},
};
}
}
using System;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace TotalRows
{
public class TotalItemsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is CollectionViewGroup group)
{
switch (parameter)
{
case "13": return group.Items.OfType<ItemClass>().Sum(item => item.Y2013);
case "14": return group.Items.OfType<ItemClass>().Sum(item => item.Y2014);
case "15": return group.Items.OfType<ItemClass>().Sum(item => item.Y2015);
case "16": return group.Items.OfType<ItemClass>().Sum(item => item.Y2016);
}
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public static TotalItemsConverter Instance { get; } = new TotalItemsConverter();
}
public class TotalItemsConverterExtension : MarkupExtension
{
public override object ProvideValue(IServiceProvider serviceProvider)
=> TotalItemsConverter.Instance;
}
}
<Window x:Class="TotalRows.TotalRowsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TotalRows"
mc:Ignorable="d"
Title="RowsTotalWindow" Height="450" Width="800">
<FrameworkElement.Resources>
<CollectionViewSource x:Key="items" Source="{x:Static local:ExampleData.Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Group"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</FrameworkElement.Resources>
<Grid>
<DataGrid ItemsSource="{Binding Mode=OneWay, Source={StaticResource items}}">
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Converter={local:TotalItemsConverter}, ConverterParameter=13}" Margin="95,0,0,0"/>
<TextBlock Text="{Binding Converter={local:TotalItemsConverter}, ConverterParameter=14}" Margin="15,0,0,0"/>
<TextBlock Text="{Binding Converter={local:TotalItemsConverter}, ConverterParameter=15}" Margin="15,0,0,0"/>
<TextBlock Text="{Binding Converter={local:TotalItemsConverter}, ConverterParameter=16}" Margin="15,0,0,0"/>
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
</Grid>
</Window>

Expanding/selecting a treeview node when it is not currently visible

I have a requirement to change the selected node in a treeview which is hosted in a separate tab.
Further more, if the parent node is not expanded, I wish to expand the node.
After about an hour of fruitless searching through SO, Google, et al, I have decided to post a question.
I can find and expand the required node when it is all visible, but when the treeveiw is obscured by another tab item, it doesn't update. I'm also not entirely sure that the item is being 'selected' - in the debugger it says ISelected is true, and the IsExpanded property of the parent is also true.
I have simplified down my actual problem in the following lines of code:
XAML (Tab control which has two items, one is a button to reproduce the problem, and a treeview which should be updated):
<Window x:Class="TreeviewTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TabControl>
<TabItem Header="Select">
<Button Content="Select Delta!" Click="ButtonBase_OnClick" />
</TabItem>
<TabItem Header="Tree">
<TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</TabItem>
</TabControl>
</Grid>
MainWindow code:
namespace TreeviewTest
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow
{
public ObservableCollection<TreeNode> Nodes { get; set; }
public ICollectionView NodesDisplay { get; set; }
public MainWindow()
{
InitializeComponent();
Nodes = new ObservableCollection<TreeNode>
{
new TreeNode(new List<TreeLeaf>
{
new TreeLeaf{Name = "Alpha"},
new TreeLeaf{Name = "Beta"}
}){ Name = "One" },
new TreeNode(new List<TreeLeaf>
{
new TreeLeaf{Name = "Delta"},
new TreeLeaf{Name = "Gamma"}
}){ Name = "Two" }
};
NodesDisplay = CollectionViewSource.GetDefaultView(Nodes);
DataContext = this;
}
public class TreeNode
{
public string Name { get; set; }
public ObservableCollection<TreeLeaf> Children { get; private set; }
public ICollectionView ChildrenDisplay { get; private set; }
public TreeNode(IEnumerable<TreeLeaf> leaves)
{
Children = new ObservableCollection<TreeLeaf>(leaves);
ChildrenDisplay = CollectionViewSource.GetDefaultView(Children);
}
}
public class TreeLeaf
{
public string Name { get; set; }
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
EnsureCanIterateThroughCollection(treTest);
var rootLevelToSelect = Nodes.First(x => x.Name == "Two");
TreeViewItem root = treTest.ItemContainerGenerator.ContainerFromItem(rootLevelToSelect) as TreeViewItem;
EnsureCanIterateThroughCollection(root);
var leafLevelToSelect = rootLevelToSelect.Children.First(x => x.Name == "Delta");
TreeViewItem leaf = root.ItemContainerGenerator.ContainerFromItem(leafLevelToSelect) as TreeViewItem;
if (!root.IsExpanded)
root.IsExpanded = true;
leaf.IsSelected = true;
ReflectivelySelectTreeviewItem(leaf);
}
//Got this from another SO post - not sure is setting IsSelected on the node is actually doing what I think it is...
private static void ReflectivelySelectTreeviewItem(TreeViewItem node)
{
MethodInfo selectMethod = typeof(TreeViewItem).GetMethod("Select", BindingFlags.NonPublic | BindingFlags.Instance);
selectMethod.Invoke(node, new object[] { true });
}
private static void EnsureCanIterateThroughCollection(ItemsControl itemsControl)
{
if (itemsControl.ItemContainerGenerator.Status == GeneratorStatus.NotStarted)
ForceGenerateChildContent(itemsControl);
}
private static void ForceGenerateChildContent(ItemsControl itemsControl)
{
itemsControl.ApplyTemplate();
IItemContainerGenerator generator = itemsControl.ItemContainerGenerator;
GeneratorPosition position = generator.GeneratorPositionFromIndex(0);
using (generator.StartAt(position, GeneratorDirection.Forward, true))
{
for (int i = 0; i < itemsControl.Items.Count; i++)
{
DependencyObject dp = generator.GenerateNext();
generator.PrepareItemContainer(dp);
}
}
}
}
}
Also - another XAML snippet which does the same thing, but where the treeview is visible - you should be able to see the treeview expand and the item selected
<Window x:Class="TreeviewTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Button Content="Select Delta!" Click="ButtonBase_OnClick" />
<TreeView ItemsSource="{Binding NodesDisplay}" Name ="treTest" Grid.Row="1">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ChildrenDisplay}">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
I would appreciate any help - My understanding of treeview's and WPF and databinding is that it doesn't work as nicely as something that gives you IsSynchronisedWithCurrentItem, which is why I am trying to handle updates to the treeview manually and am also trying to select items in the tree programmatically. If I am wrong on that front I would love a pointer to show me how to do it in a more 'WPF' way!
When a tab page is not visible, the controls are not created. Only once you switch to it are the controls created. Add a Boolean to your TreeNode viewmodel and bind the IsSelected property.
TreeNodeVm.cs:
using Microsoft.Practices.Prism.ViewModel;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace TreeViewSelectTest
{
public class TreeNodeVm : NotificationObject
{
private TreeNodeVm Parent { get; set; }
private bool _isSelected = false;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
RaisePropertyChanged(() => IsSelected);
}
}
private bool _isExpanded = false;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
RaisePropertyChanged(() => IsExpanded);
}
}
public ObservableCollection<TreeNodeVm> Children { get; private set; }
public string Header { get; set; }
public TreeNodeVm()
{
this.Children = new ObservableCollection<TreeNodeVm>();
this.Children.CollectionChanged += Children_CollectionChanged;
}
void Children_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (var newChild in e.NewItems.Cast<TreeNodeVm>())
{
newChild.Parent = this;
}
}
}
public TreeNodeVm(string header, IEnumerable<TreeNodeVm> children)
: this()
{
this.Header = header;
foreach (var child in children)
Children.Add(child);
}
public void MakeVisible()
{
if (Parent != null)
{
Parent.MakeVisible();
}
this.IsExpanded = true;
}
public void Select()
{
MakeVisible();
this.IsSelected = true;
}
}
}
MainWindow.xaml:
<Window x:Class="TreeViewSelectTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button Content="Select B1" Click="btSelectB1_Click" />
</StackPanel>
<TabControl>
<TabItem Header="treeview">
<TreeView ItemsSource="{Binding Path=RootNode.Children}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding Path=IsSelected,Mode=TwoWay}" />
<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded,Mode=TwoWay}" />
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</TabItem>
<TabItem Header="the other item">
<Button />
</TabItem>
</TabControl>
</DockPanel>
</Window>
MainWindow.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TreeViewSelectTest
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
RootNode = new TreeNodeVm("Root", new[]
{
new TreeNodeVm("A", new [] {
new TreeNodeVm("A1", new TreeNodeVm[0]),
new TreeNodeVm("A2", new TreeNodeVm[0]),
new TreeNodeVm("A3", new TreeNodeVm[0])
}),
new TreeNodeVm("B", new [] {
new TreeNodeVm("B1", new TreeNodeVm[0])
})
});
InitializeComponent();
this.DataContext = this;
}
public TreeNodeVm RootNode { get; private set; }
private void btSelectB1_Click(object sender, RoutedEventArgs e)
{
RootNode.Children[1].Children[0].Select();
}
}
}
When you call TreeNodeVm.Select(), it will update the future state of the visuals. Once the tab page gets switched back, the template is applied and the visuals are created as expanded and selected.

Categories

Resources