WPF TreeView in ControlTemplate - Handling Item Expanded - wpf-controls

I am creating a Custom Control and in the control template I have a TreeView class in it.
<Style TargetType="{x:Type local:MyControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyControl}">
<Border>
<TreeView ItemsSource="{Binding TreeDataItems, RelativeSource={RelativeSource TemplatedParent}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TreeViewItem.Expanded">
<i:InvokeCommandAction Command="{Binding TreeItemExpandedCommand, RelativeSource={RelativeSource TemplatedParent}}"
CommandParameter="{Binding}">
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TreeView>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
In the code file for the custom control I have this command:
private ICommand _TreeItemExpandedCommand;
public ICommand TreeItemExpandedCommand
{
get
{
if (_TreeItemExpandedCommand == null)
_TreeItemExpandedCommand = new RelayCommand(p => TreeItemExpandedExecuted(p));
return _TreeItemExpandedCommand;
}
}
private void TreeItemExpandedExecuted(object args)
{
}
I have also tried
<TreeView TreeViewItem.Expanded="TreeViewItem_Expanded">
...
</TreeView/>
but neither gets fired.
How can I handle the TreeView's Expanded event INSIDE my custom control's code file?
Thanks

I am not pro Wpf programmer, but i manage to figure out the solution for your problem. The problem with your code, is that the event is not exactly belongs to TreeView itself, but rather the elements inside it (Items property).
So this wont work:
...
<i:EventTrigger EventName="TreeViewItem.Expanded">
...
On the other hand, this would work:
...
<i:EventTrigger EventName="SelectedItemChanged">
...
because this event belongs to TreeView
Basicly, what you need to do is subscribe for the Expanded event of all the TreeViewItems in the TreeView. You can do this from code behind.
Here is how i did it:
public class MyCustomControl : Control
{
public TreeView MyTreeView { get; set; }
public static readonly DependencyProperty TreeViewItemListProperty = DependencyProperty.Register("TreeViewItemList",
typeof(List<StorageItem>),
typeof(MyCustomControl),
new UIPropertyMetadata(null));
public List<StorageItem> TreeViewItemList
{
get { return (List<StorageItem>)GetValue(TreeViewItemListProperty); }
set { SetValue(TreeViewItemListProperty, value);}
}
public static readonly RoutedEvent MyEventRoutedEvent = EventManager.RegisterRoutedEvent("MyEvent",
RoutingStrategy.Bubble,
typeof(EventHandler<RoutedEventArgs>),
typeof(MyCustomControl));
public event RoutedEventHandler MyEvent
{
add { this.AddHandler(MyEventRoutedEvent, value); }
remove { this.RemoveHandler(MyEventRoutedEvent, value); }
}
public override void OnApplyTemplate()
{
MyTreeView = Template.FindName("MyTreeView", this) as TreeView;
MyTreeView.Items.CurrentChanged += Items_CurrentChanged;
base.OnApplyTemplate();
}
private void Items_CurrentChanged(object sender, EventArgs e)
{
SubscribeAllTreeViewItems(MyTreeView);
}
private void SubscribeAllTreeViewItems(ItemsControl treeViewItem)
{
foreach (object item in treeViewItem.Items)
{
TreeViewItem treeItem = treeViewItem.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (treeItem != null)
{
SubscribeAllTreeViewItems(treeItem);
treeItem.Expanded += TreeViewItem_Expanded;
}
}
}
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
RoutedEventArgs args = new RoutedEventArgs(MyEventRoutedEvent, this);
this.RaiseEvent(args);
}
static MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl), new FrameworkPropertyMetadata(typeof(MyCustomControl)));
}
}
First we need to acquire our TreeView Control and subscribe the event if it gets items:
public override void OnApplyTemplate()
{
MyTreeView = Template.FindName("MyTreeView", this) as TreeView;
MyTreeView.Items.CurrentChanged += Items_CurrentChanged;
base.OnApplyTemplate();
}
Then, if this is fired, we know that our TreeView has Items in it, so we can cycle through its elements and subscribe for the event Expanded:
private void Items_CurrentChanged(object sender, EventArgs e)
{
SubscribeAllTreeViewItems(MyTreeView);
}
private void SubscribeAllTreeViewItems(ItemsControl treeViewItem)
{
foreach (object item in treeViewItem.Items)
{
TreeViewItem treeItem = treeViewItem.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (treeItem != null)
{
SubscribeAllTreeViewItems(treeItem);
treeItem.Expanded += TreeViewItem_Expanded;
}
}
}
After that, we are good to go! I also added code for to be able to subscribe for this event from outside the custom control:
public static readonly RoutedEvent MyEventRoutedEvent = EventManager.RegisterRoutedEvent("MyEvent",
RoutingStrategy.Bubble,
typeof(EventHandler<RoutedEventArgs>),
typeof(MyCustomControl));
public event RoutedEventHandler MyEvent
{
add { this.AddHandler(MyEventRoutedEvent, value); }
remove { this.RemoveHandler(MyEventRoutedEvent, value); }
}
....
....
private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
RoutedEventArgs args = new RoutedEventArgs(MyEventRoutedEvent, this);
this.RaiseEvent(args);
}
So this way, you can subscribe it from outside like that:
<local:MyCustomControl TreeViewItemList="{Binding Path=Items}"
MyEvent="MyCustomControl_MyEvent"/>
Finally, here it is the .xaml
<Style TargetType="{x:Type local:MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomControl}">
<TreeView ItemsSource="{TemplateBinding TreeViewItemList}"
x:Name="MyTreeView">
<TreeView.Resources>
<!--Here we specify how to display a FolderItem-->
<HierarchicalDataTemplate DataType="{x:Type localviewmodels:FolderItem}"
ItemsSource="{Binding Path=Items}">
<TextBlock Text="{Binding Path=Name}"
Margin="0 0 35 0"/>
</HierarchicalDataTemplate>
<!--Here we specify how to display a FileItem-->
<DataTemplate DataType="{x:Type localviewmodels:FileItem}">
<TextBlock Text="{Binding Path=Name}"
Margin="0 0 35 0"/>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
UPDATE
if you want to set TreeView items inside the custom Control, you should
subscribe the MyTreeView.Loaded event as well to catch the first time when it gets values.
i hope it helps!
If anything is not clear, feel free to ask, i am ready to help!

Related

Specify default ItemTemplate

I have a WPF custom Control in which I have a Listview. The control has dependancy properties for the ItemSource and ItemTemplate of the ListView. This all works fine. What I would like to do is to be able to set a default ItemTemplate so that I don't end up with object.ToString() for the Items in the Listview.
Below is the Xaml Style for my control.
<ResourceDictionary 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:btl="clr-namespace:Btl.Controls"
mc:Ignorable="d">
<DataTemplate x:Key="DefaultListViewItem" DataType="btl:SelectableItem">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="2" IsChecked="{Binding Selected}" />
<TextBlock Margin="5,2" Text="{Binding Description}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<Style TargetType="{x:Type btl:SelectItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type btl:SelectItemsControl}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock x:Name="PART_Title" Margin="10"
Text="{Binding Path=Title,
RelativeSource={RelativeSource TemplatedParent}}"
TextWrapping="Wrap"
Visibility="Collapsed"/>
<GroupBox Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<GroupBox.Header>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="PART_EnabledCheck" Margin="0,5"
Content=""
IsChecked="{Binding Path=EnabledCheck, Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock x:Name="PART_GroupTitle" VerticalAlignment="Center"
Text="{Binding Path=GroupTitle,
RelativeSource={RelativeSource TemplatedParent}}"/>
</StackPanel>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<ListView x:Name="PART_Items"
ItemsSource="{Binding ItemSourceList,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type btl:SelectItemsControl}}}"
ItemTemplate="{Binding ItemTemplate,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type btl:SelectItemsControl}}}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="2"
>
</ListView>
</DockPanel>
<CheckBox x:Name="PART_SelectAllCheck" Grid.Row="1" Margin="11,5" Content="Select All"
IsChecked="{Binding Selected}"/>
</Grid>
</GroupBox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is my control
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace Btl.Controls
{
/// <summary>
///
/// </summary>
///
[TemplatePart(Name = "PART_Title", Type = typeof(TextBlock))]
[TemplatePart(Name = "PART_EnabledCheck", Type = typeof(CheckBox))]
[TemplatePart(Name = "PART_GroupTitle", Type = typeof(TextBlock))]
[TemplatePart(Name = "PART_Items", Type = typeof(ListView))]
[TemplatePart(Name = "PART_SelectAllCheck", Type = typeof(CheckBox))]
public class SelectItemsControl : UserControl
{
#region DependencyProperties
#region Title
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
// Using a DependencyProperty as the backing store for Title. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register("Title", typeof(string), typeof(SelectItemsControl), new PropertyMetadata(string.Empty,
OnTitleChanged
));
private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as TextBlock;
if (control != null)
if (string.IsNullOrEmpty(e.NewValue.ToString()))
{
control.Visibility = string.IsNullOrEmpty(e.NewValue.ToString()) ? Visibility.Collapsed : Visibility.Visible;
}
}
#endregion
#region HasEnabledCheck
public bool HasEnabledCheck
{
get { return (bool)GetValue(HasEnabledCheckProperty); }
set { SetValue(HasEnabledCheckProperty, value); }
}
// Using a DependencyProperty as the backing store for EnabledCheck. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HasEnabledCheckProperty =
DependencyProperty.Register("HasEnabledCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));
#endregion
#region EnabledCheck
public bool EnabledCheck
{
get { return (bool)GetValue(EnabledCheckProperty); }
set { SetValue(EnabledCheckProperty, value); }
}
// Using a DependencyProperty as the backing store for EnabledCheck. This enables animation, styling, binding, etc...
public static readonly DependencyProperty EnabledCheckProperty =
DependencyProperty.Register("EnabledCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(true));
#endregion
#region GroupTitle
public string GroupTitle
{
get { return (string)GetValue(GroupTitleProperty); }
set { SetValue(GroupTitleProperty, value); }
}
// Using a DependencyProperty as the backing store for GroupTitle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GroupTitleProperty =
DependencyProperty.Register("GroupTitle", typeof(string), typeof(SelectItemsControl), new UIPropertyMetadata(""));
#endregion
#region ItemSourceList
public IEnumerable<ISelectable> ItemSourceList
{
get { return (IEnumerable<ISelectable>)GetValue(ItemSourceListProperty); }
set { SetValue(ItemSourceListProperty, value); }
}
// Using a DependencyProperty as the backing store for Items. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemSourceListProperty =
DependencyProperty.Register("ItemSourceList", typeof(IEnumerable), typeof(SelectItemsControl));
#endregion
#region ItemTemplate
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
new UIPropertyMetadata(default(DataTemplate)));
#endregion
#region DescriptionTemplate
public DataTemplate DescriptionTemplate
{
get { return (DataTemplate)GetValue(DescriptionTemplateProperty); }
set { SetValue(DescriptionTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DescriptionTemplateProperty =
DependencyProperty.Register("DescriptionTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
new UIPropertyMetadata(default(DataTemplate), OnDescriptionTemplateChanged));
private static void OnDescriptionTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
}
#endregion
#region ItemSelected
public bool ItemSelected
{
get { return (bool)GetValue(ItemSelectedProperty); }
set { SetValue(ItemSelectedProperty, value); }
}
// Using a DependencyProperty as the backing store for EnabledCheck. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemSelectedProperty =
DependencyProperty.Register("ItemSelectedCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));
#endregion
#region ItemDescription
public string ItemDescription
{
get { return (string)GetValue(ItemDescriptionProperty); }
set { SetValue(ItemDescriptionProperty, value); }
}
// Using a DependencyProperty as the backing store for ItemDescription. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemDescriptionProperty =
DependencyProperty.Register("ItemDescription", typeof(string), typeof(SelectItemsControl), new UIPropertyMetadata(""));
#endregion
#region SelectAllCheck
public bool SelectAll
{
get { return (bool)GetValue(SelectAllProperty); }
set { SetValue(SelectAllProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectAll. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SelectAllProperty =
DependencyProperty.Register("SelectAll", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));
#endregion
#endregion
#region Private Members
private TextBlock _partTitle;
private CheckBox _partEnabledCheck;
private TextBlock _partGroupTitle;
private ListView _partItemsListView;
private CheckBox _partSelectAllCheck;
#endregion
/// <summary>
///
/// </summary>
static SelectItemsControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectItemsControl),
new FrameworkPropertyMetadata(typeof(SelectItemsControl)));
}
public SelectItemsControl()
{
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
if (ItemTemplate == null)
{
CreateDefaultItemTemplate();
}
PresentationSource presentationSource = PresentationSource.FromVisual((Visual)sender);
// Subscribe to PresentationSource's ContentRendered event
// ReSharper disable once PossibleNullReferenceException
presentationSource.ContentRendered += SelectItemsControl_ContentRendered;
}
private void SelectItemsControl_ContentRendered(object sender, EventArgs e)
{
// Don't forget to unsubscribe from the event
((PresentationSource)sender).ContentRendered -= SelectItemsControl_ContentRendered;
ListenToSelectedCheckBoxClickEvent(_partItemsListView, true);
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
ListenToSelectedCheckBoxClickEvent(_partItemsListView, false);
}
private void CreateDefaultItemTemplate()
{
DataTemplate template = new DataTemplate { DataType = typeof(ListViewItem) };
FrameworkElementFactory stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));
stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
FrameworkElementFactory selected = new FrameworkElementFactory(typeof(CheckBox));
selected.SetBinding(TextBlock.TextProperty, new Binding("Selected"));
stackPanelFactory.AppendChild(selected);
FrameworkElementFactory title = new FrameworkElementFactory(typeof(TextBlock));
title.SetBinding(TextBlock.TextProperty, new Binding("Description"));
stackPanelFactory.AppendChild(title);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Code to get the Template parts as instance member
_partTitle = GetTemplateChild("PART_Title") as TextBlock;
_partEnabledCheck = GetTemplateChild("PART_EnabledCheck") as CheckBox;
_partGroupTitle = GetTemplateChild("PART_GroupTitle") as TextBlock;
_partItemsListView = GetTemplateChild("PART_Items") as ListView;
//_partItemSelectedCheck = GetTemplateChild("PART_ItemSelectedCheck") as CheckBox;
_partSelectAllCheck = GetTemplateChild("PART_SelectAllCheck") as CheckBox;
if (_partTitle == null || _partEnabledCheck == null || _partGroupTitle == null || _partItemsListView == null ||
_partSelectAllCheck == null)
{
throw new NullReferenceException("Template parts not available");
}
// set visibility
_partEnabledCheck.Visibility = HasEnabledCheck ? Visibility.Visible : Visibility.Collapsed;
_partEnabledCheck.Click += PartEnabledCheckOnClick;
_partTitle.Visibility = string.IsNullOrEmpty(_partTitle.Text) ? Visibility.Collapsed : Visibility.Visible;
_partGroupTitle.Visibility = string.IsNullOrEmpty(_partGroupTitle.Text) ? Visibility.Collapsed : Visibility.Visible;
_partSelectAllCheck.Click += PartSelectAllCheckOnClick;
}
private void PartEnabledCheckOnClick(object sender, RoutedEventArgs routedEventArgs)
{
_partItemsListView.IsEnabled = EnabledCheck;
_partSelectAllCheck.IsEnabled = EnabledCheck;
}
private void ListenToSelectedCheckBoxClickEvent(DependencyObject parent, bool set)
{
foreach (CheckBox cb in VisualTreeHelpers.FindVisualChildren<CheckBox>(parent))
{
BindingExpression binding = cb.GetBindingExpression(CheckBox.IsCheckedProperty);
// ReSharper disable once PossibleNullReferenceException
if (binding.ParentBinding.Path.Path == "Selected")
{
if (set)
cb.Click += SelectedCheckBox_Click;
else
cb.Click -= SelectedCheckBox_Click;
}
}
}
private void SelectedCheckBox_Click(object sender, RoutedEventArgs e)
{
_partSelectAllCheck.IsChecked = !ItemSourceList.AsQueryable().Any(x => x.Selected == false);
}
private void PartSelectAllCheckOnClick(object sender, RoutedEventArgs routedEventArgs)
{
foreach (CheckBox cb in VisualTreeHelpers.FindVisualChildren<CheckBox>(_partItemsListView))
{
BindingExpression binding = cb.GetBindingExpression(CheckBox.IsCheckedProperty);
// ReSharper disable once PossibleNullReferenceException
if (binding.ParentBinding.Path.Path == "Selected")
{
cb.IsChecked = _partSelectAllCheck.IsChecked ?? false;
}
}
}
}
}
Could someone please post some code which shows how set - create the default template?
This turned out to be simpler than I thought. Because the ItemTemplate is bound to a dependency property I can specify the default template there. That just left the creation of the template. See below.
#region ItemTemplate
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
new UIPropertyMetadata(DefaultItemTemplate));
private static DataTemplate DefaultItemTemplate
{
get
{
// tried using a MemoryStream - StreamWriter but was getting a
// "Root element missing error", would be nice to know why.
var sb = new StringBuilder();
sb.Append("<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">");
sb.Append("<StackPanel Orientation=\"Horizontal\">");
sb.Append("<CheckBox Margin=\"2\" IsChecked=\"{Binding Selected}\" />");
sb.Append("<TextBlock Margin=\"5,2\" Text=\"{Binding Description}\" VerticalAlignment=\"Center\"/>");
sb.Append("</StackPanel>");
sb.Append("</DataTemplate>");
var myByteArray = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
var ms = new MemoryStream(myByteArray);
return (DataTemplate) XamlReader.Load(ms);
}
}
#endregion

Updating a ListView with Xamarin.Forms

I am having an issue with list views on in a couple of my Xamarin Forms applications. One form is within a tabbed page setup, the other is a normal content page (different apps)
I have a class like this
public class SomeClass
{
public string StringOne {get;set;}
public string StringTwo {get;set;}
public int IntOne {get;set;}
}
In my Content page, I set up an ObservableCollection and add some data in. I then tell the list that SomeClass is my ItemSource. This produces the ListView correctly on all of my devices.
The problem is that when I change one of the properties, nothing on the ListView changes (so if say I have 3 objects in the Observable and remove one, the list still says 3 - or if I change a property in my second object, the second item on the ListView doesn't change either).
I have also tried to solve the problem by using a standard List and implement INotifyChanged within the class. Again though, the ListView doesn't alter when the List changes.
I know the data has altered as if I make a change to the object, come out and go back in, the data has changed in the UI.
Am I doing something wrong or is this a bug I need to putting into Bugzilla?
It will not change if you don't bind it and implement INotifyPropertyChanged interface.
Sample Code:
public class ObservableProperty : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class SomeClass:ObservableProperty
{
string stringOne;
string stringTwo;
int intOne;
public string StringOne
{
get{return stringOne;}
set
{
stringOne = value;
OnPropertyChanged("StringOne");
}
}
public string StringTwo
{
get{ return stringTwo;}
set
{
stringTwo = value;
OnPropertyChanged("StringTwo");
}
}
public int IntOne
{
get{ return intOne;}
set
{
intOne = value;
OnPropertyChanged("IntOne");
}
}
}
public class MainVM:ObservableProperty
{
ObservableCollection<SomeClass> items;
public ObservableCollection<SomeClass> items
{
get{return items;}
set
{
items = value;
OnPropertyChanged("Items");
}
}
public MainVM()
{
Items = new ObservableCollection<SomeClass>();
Items.Add(new SomeClass(){StringOne = "123", StringTwo = "test", IntOne =12});
}
public void CallMeForChangingProperty()
{
SomeClass item = Items[0];
item.StringOne = "Test1";
}
}
public class MainView
{
public MainView()
{
this.BindingContext= new MainVM()
}
}
< ListView ItemsSource="{Binding Items}" RowHeight="120">
< ListView.ItemTemplate>
< DataTemplate>
< ViewCell>
< ViewCell.View>
< StackLayout>
< Label Text= "StringOne" />
< Label Text= "StringTwo" />
< Label Text= "IntOne" />
</ StackLayout>
</ ViewCell.View>
</ ViewCell>
</ DataTemplate>
</ ListView.ItemTemplate>
</ ListView>
Answer given by #eakgul works like a charm for me.
I'll attach here what I've implemented, maybe it could help someone.
You have to set INotifyPropertyChanged both, to the ObservableColection and to it's itens.
I have a BaseViewModel with INotifyPropertyChanged as follows:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void SetProperty<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals( backingField, value)) return;
backingField = value;
OnPropertyChanged(propertyName);
}
}
On my BluetoothPage.xaml, first I set bindincontext to my BluetoothPageViewModel.cs and set the ListView ItemsSource and it's binded labels:
<ContentPage.BindingContext>
<viewmodel:BluetoothPageViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<StackLayout Padding="5,10">
<Button x:Name="Scan_Devices_Button"
Command="{Binding SearchNew_Button_Clicked}"/>
<ListView x:Name="DevicesList"
ItemsSource="{Binding BluetoothDevices}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
IsPullToRefreshEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding device.Device.NativeDevice.Name}"/>
<Label Grid.Column="1"
Text="{Binding device.Device.NativeDevice.Address, StringFormat='ID: {0}'}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding device.Rssi, StringFormat='Power: {0:F2}dbm'}"/>
<Label Grid.Column="1"
Text="{Binding distance, StringFormat='Distance: {0:F2}m'}"/>
</Grid>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
Then, in my BluetoothPageViewModel.cs I extend it with the BaseViewModel and declare ItemsSource BluetoothDevices with INotifyPropertyChanged. At this moment, everytime I change an item on the ObservableCollection BluetoothDevices, the ListView will be updated. But, If I made a change on an item inside the ObservableCollection, nothing will happen!
So, you must set INotifyPropertyChanged to it's itens.
Following is my BluetoothPageViewModel, which uses a class BluetoothPageModel in the PageModel BluetoothPageModel.cs
The BluetoothPageViewModel:
public class BluetoothPageViewModel : BaseViewModel
{
public BluetoothPageViewModel()
{
SearchNew_Button_Clicked = new Command(NewDevices_Button_Clicked_Event);
Scan_Devices_Button_BgColor = "#D6D7D7";
Scan_Devices_Button_Text = "Scan nearby devices";
}
#region Declarations
public List<IDevice> iDeviceList = new List<IDevice>();
public ObservableCollection<BluetoothPageModel> _bluetoothDevices = new ObservableCollection<BluetoothPageModel>();
public BluetoothPageModel _selectedItem;
public ObservableCollection<BluetoothPageModel> BluetoothDevices
{
get { return _bluetoothDevices; }
set { SetProperty(ref _bluetoothDevices, value); }
}
public BluetoothPageModel SelectedItem
{
get { return _selectedItem; }
set { SetProperty(ref _selectedItem, value); }
}
public ICommand SearchNew_Button_Clicked { get; private set; }
#endregion
#region Functions
private void NewDevices_Button_Clicked_Event(object obj)
{
// discover some devices
if (!CrossBleAdapter.Current.IsScanning)
{
BluetoothDevices.Clear();
iDeviceList.Clear();
var scanner = CrossBleAdapter.Current.Scan().Subscribe(scanResult =>
{
if (!iDeviceList.Contains(scanResult.Device))
{
iDeviceList.Add(scanResult.Device);
Device.BeginInvokeOnMainThread(() =>
{
BluetoothDevices.Add(new BluetoothPageModel
{
device = scanResult,
distance = Math.Pow(10, ((-68 - scanResult.Rssi) / 31.1474))
});
});
}
else
{
int ind = iDeviceList.IndexOf(scanResult.Device);
Device.BeginInvokeOnMainThread(() =>
{
BluetoothDevices[ind].device = scanResult;
BluetoothDevices[ind].distance = Math.Pow(10, ((-68 - scanResult.Rssi) / 31.1474));
});
}
});
}
else
{
CrossBleAdapter.Current.StopScan(); //When you want to stop scanning
}
}
#endregion
}
Finally, to be able to update data when you change a property of the BluetoothPageModel class:
public class BluetoothPageModel:BaseViewModel
{
public IScanResult _device;
public double _distance;
public IScanResult device
{
get { return _device; }
set { SetProperty(ref _device, value); }
}
public double distance
{
get { return _distance; }
set { SetProperty(ref _distance, value); }
}
}
Thanks to eakgul answer I could get it working. Hope it can help someone else.

Set property of UserControl from code behind of parent during page initialization

I have a page with a user control on it, and the user control has a dependency property. The logic for setting the value of the property is slightly complicated so I want to do from the code-behind for the page.
The intended flow is:
MainPage:
- In Loaded event, set property on control
ChildControl
- In Loaded event, push property to XAML
I've tried this in WPF and WinRT, and followed it with breakpoints in the debugger. It works in WPF as intended, but in WinRT the Loaded event for the child control is called before the event for the MainPage, so the sequence fails.
ChildControl.xaml
<UserControl x:Class="UserControlFromWinRT.ChildControl"
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"
Loaded="UserControl_Loaded">
<Grid>
<TextBlock x:Name="greetingTextBlock"/>
</Grid>
ChildControl.xaml.cs
public partial class ChildControl : UserControl
{
public ChildControl() {
InitializeComponent();
}
public string Greeting {
get { return (string)GetValue(GreetingProperty); }
set { SetValue(GreetingProperty, value); }
}
public static readonly DependencyProperty GreetingProperty =
DependencyProperty.Register("Greeting", typeof(string), typeof(ChildControl), new PropertyMetadata(""));
private void UserControl_Loaded(object sender, RoutedEventArgs e) {
greetingTextBlock.Text = Greeting;
}
}
MainPage.xaml
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<local:ChildControl x:Name="childControl" Margin="20,30,0,0" FontSize="30"/>
</Grid>
MainPage.xaml.cs
private void Page_Loaded(object sender, RoutedEventArgs e) {
childControl.Greeting = "Hello";
}
You need to add a PropertyChangedCallback to your PropertyMetadata Constructor
Like:
public static readonly DependencyProperty GreetingProperty = DependencyProperty.Register("Greeting", typeof(string), typeof(ChildControl), new PropertyMetadata("", OnGreetingChanged));
private static void OnGreetingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// do something e.NewValue
}

log4net WPF UserControl Appender won't update RichTextBox during the DoAppend event using a BackgroundWorker

I'm wondering if anybody has ever run across this issue. I'm trying to show all the logging events in a RichTextBox of a WPF application, and thought I could use the IAppender interface on the UserControl to tack on a global Appender.
I'm using a BackgroundWorker to run a "long running" process in the background and create logger.Info("text") events during the DoWork event.
The issue I'm having is that the DoAppend event fires, and it appears that the text gets updated during the Dispatcher event, but the UI does not reflect this.
Here is my LogAppender class:
public partial class LogAppender : UserControl, IAppender
{
public LogAppender()
{
InitializeComponent();
}
public void Close()
{
}
public log4net.Layout.PatternLayout Layout
{
get;
set;
}
public void DoAppend(log4net.Core.LoggingEvent loggingEvent)
{
this.LogTextBlock.Dispatcher.Invoke(
DispatcherPriority.Normal,
new Action(
delegate
{
StringWriter writer = new StringWriter();
this.Layout.Format(writer, loggingEvent);
this.LogTextBlock.AppendText(writer.ToString());
}));
}
}
<UserControl>
<Grid>
<RichTextBox x:Name="LogTextBlock"/>
</Grid>
</UserControl>
Here is my MainWindow.xaml button click and backgroundworker code:
public partial class MainWindow
{
protected static readonly ILog logger = LogManager.GetLogger(typeof(MainWindow));
public MainWindow()
{
InitializeComponent();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += bw_DoWork;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;
bw.RunWorkerAsync();
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
logger.Info("Complete");
}
void bw_DoWork(object sender, DoWorkEventArgs e)
{
logger.Info("Start");
Thread.Sleep(5000);
logger.Info("End");
}
}
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Height="30" Click="Button_Click_1">Start</Button>
<local:LogAppender Grid.Row="1" Grid.ColumnSpan="2"/>
</Grid>
I've tried just binding a TextBlock to the text being generated by the DoAppend method as well as just setting the Text property of a TextBlock, to no avail.
I'm sure I'm missing some small but huge point, but I scratched my head for about 3 hours this afternoon to no avail.
Any help is appreciated!!
Thanks
Bryan
The log4net Appender here might help you. In essence, Pete has implemented a lognet Appender that supports INotifyPropertyChanged and exposes a property that is the concatenation of all of the messages logged so far. The sample program that is available via a link from the page that I linked above shows how to make your view bind to the output of the Appender. I have not actually used it, so I can't comment on how useful (or not) it is, but it does seem to be trying to solve the same problem that you are trying to solve.
Good luck!
Unfortunately, I was not able to find a quick solution to this particular problem. What I did do to solve my problem of getting log4net logs into a WPF form was create a bit of a hack (or maybe not??).
I created a singleton that exposes the log4net "Log" text one line at a time (didn't want to use up much memory). Basically, the SingletonAppender has one property on it, "Log", that gets changed or updated during the DoAppend event of the custom IAppender shown below.
public class SingletonAppender
{
private static volatile SingletonAppender instance;
private static object syncRoot = new Object();
private string log = string.Empty;
private SingletonAppender()
{
}
public static SingletonAppender Instance
{
get
{
if (instance == null)
{
lock (syncRoot)
{
if (instance == null)
{
instance = new SingletonAppender();
}
}
}
return instance;
}
}
public string Log
{
get
{
string retValue = this.log;
// whenever we "get" the Log property, reset the underlying value to empty
this.log = string.Empty;
return retValue;
}
set
{
this.log = value;
}
}
}
public class LogAppender : IAppender
{
private string name = string.Empty;
public LogAppender()
{
}
#region IAppender Members
public void Close()
{
}
public void DoAppend(log4net.Core.LoggingEvent loggingEvent)
{
string retValue = string.Empty;
if (this.Layout != null)
{
StringWriter writer = new StringWriter();
this.Layout.Format(writer, loggingEvent);
retValue = writer.ToString();
}
else
{
retValue = loggingEvent.MessageObject.ToString();
}
SingletonAppender.Instance.Log += retValue;
}
public log4net.Layout.PatternLayout Layout
{
get;
set;
}
public string Name
{
get
{
return this.name;
}
set
{
this.name = value;
}
}
#endregion
}
I create a "heart beat" Timer in the backgroundworker to get the log events that are happening during the process:
public partial class MainWindow
{
protected static readonly ILog logger = LogManager.GetLogger(typeof(MainWindow));
public MainWindow()
{
InitializeComponent();
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += bw_DoWork;
bw.ProgressChanged += bw_ProgressChanged;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;
bw.RunWorkerAsync();
}
void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.LogTextBox.AppendText(SingletonAppender.Instance.Log);
}
void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
logger.Info("Complete");
}
void bw_DoWork(object sender, DoWorkEventArgs e)
{
logger.Info("Start");
Timer timer = new Timer(new TimerCallback(TimerCallback), sender, 500, 2000);
Thread.Sleep(5000);
logger.Info("End");
}
public void TimerCallback(object state)
{
if (state != null)
{
BackgroundWorker worker = state as BackgroundWorker;
if (worker != null && worker.IsBusy)
{
worker.ReportProgress(0, null);
}
}
}
}
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button Height="30" Click="Button_Click_1">Start</Button>
<RichTextBox x:Name="LogTextBox"/>
</Grid>

WPF how to see last added text line in TexBox

I would like simmulate Console text output in my WPF app
but when I add new lines in TextBox I should use scroll bar to see last added text but I want to see last added text but for firsts lines use scroll bar
<TextBox TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"
Text="{Binding Path=Data, Mode=TwoWay}" />`
Use the ScrollToLine method of TextBox (and the LineCount property to know how many lines there are) after adding text in order to make sure that the just-added line is visible.
Please consider scrolling the textbox directly from code behind like this (e.g. when text changes):
private void SampleTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (SampleTextBox.LineCount != -1)
{
SampleTextBox.ScrollToLine(SampleTextBox.LineCount - 1);
}
}
Please tell me if this helps.
thanks for answers: I expected to do it from XAML but as I undestood it's only possible from code behind
so here is my implimentation now with checkbox for stop ScrollToEnd function:
public partial class MainWindow : Window
{
private bool isScrollToEnd;
Timer timer;
public double WaitTime
{
get { return waitTime / 1000; }
set { waitTime = value * 1000; }
}
private double waitTime;
public MainWindow()
{
InitializeComponent();
isScrollToEnd = true;
waitTime = 5000;
tbWaitTime.DataContext = this;
timer = new Timer(waitTime);
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
}
// событие изменения текста в контроле tbConsole
private void tbConsole_TextChanged(object sender, TextChangedEventArgs e)
{
if (tbConsole.LineCount != -1 && isScrollToEnd)
{
tbConsole.ScrollToLine(tbConsole.LineCount - 1);
cbIsScrolling.IsChecked = false;
}
}
private void cbIsScrolling_Click(object sender, RoutedEventArgs e)
{
if ((bool)cbIsScrolling.IsChecked)
{
isScrollToEnd = !(bool)cbIsScrolling.IsChecked;
isScrollToEnd = false;
timer.Interval = waitTime;
timer.Start();
return;
}
isScrollToEnd = true;
timer.Stop();
cbIsScrolling.IsChecked = false;
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
timer.Stop();
isScrollToEnd = true;
}
}
and here is XAML code:
<StackPanel Grid.Column="1" Grid.Row="2" Grid.ColumnSpan="2" Grid.RowSpan="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,3,10,2" VerticalAlignment="Top">
<Label Content="Stop autoscrolling for:" />
<TextBox Name="tbWaitTime" Text="{Binding Path=WaitTime}"
MinWidth="25" MaxWidth="50" Margin="5,0,0,0" />
<Label Content="sec."/>
<CheckBox Name="cbIsScrolling"
HorizontalAlignment="Right" VerticalAlignment="Center"
Click="cbIsScrolling_Click" />
</StackPanel>
<TextBox Name="tbConsole"
Background="LightGoldenrodYellow" Padding="5" Height="100"
VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap"
AcceptsReturn="True"
Text="{Binding Path=Data, Mode=TwoWay}" TextChanged="tbConsole_TextChanged" />
</StackPanel>

Resources