I am working on a simple Custom Control that should go to Edit mode by double clicking on it
The concept is based on this question Click-to-edit in Silverlight
On a double click it changes initial template on Edit Template
and it seems to be pretty clear, except the part (5) How to change the template Back when the Control Looses the focus
The Lost Focus event is fired only when contained controls are loosing focus
Here is an article that talk about it
http://programmerpayback.com/2008/11/20/gotfocus-and-lostfocus-events-on-containers/
I have tried to implement same Technic but still no result, I cannot get LostFocus event working for me when I click outside of a control
Where is my issue?
My XAML
<ContentControl x:Class="Splan_RiaBusinessApplication.Controls.TimeCodeControl"
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:obj="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:behaviour="clr-namespace:Splan_RiaBusinessApplication.Behavior"
xmlns:controls="clr-namespace:Splan_RiaBusinessApplication.Controls"
xmlns:Primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
mc:Ignorable="d"
IsTabStop="True"
IsEnabled="True"
Visibility="Visible"
d:DesignHeight="100" d:DesignWidth="200"
d:Height="200" d:Width="200"
>
<ContentControl.Resources>
<ControlTemplate x:Key="DisplayTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding Target.Code, Mode=TwoWay}" />
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" >
<TextBlock Text="{Binding Target.Start, Mode=TwoWay, StringFormat=hh\\:mm }" />
<TextBlock Text='-' />
<TextBlock Text="{Binding Target.End, Mode=TwoWay, StringFormat=hh\\:mm }" />
</StackPanel>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="EditTemplate">
<Grid Background="Aqua" Height="200" Width="200">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<ComboBox Width="100" Height="25" x:Name="cbTimeCode"
ItemsSource="{Binding TimeCodes}"
SelectedValue="{Binding Target.CodeId, Mode=TwoWay}"
SelectedValuePath="TimeCodeId"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Target.Code}" />
<TextBlock Grid.Column="1" Text="{Binding Target.Description}" />
</Grid>
</DataTemplate>
</ComboBox.ItemTemplate>
<i:Interaction.Triggers>
<i:EventTrigger>
<behaviour:ResolveElementName PropertyName="ItemsSource" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
<!--<controls:TimeRangePickerControl Grid.Row="1" StartTime="{Binding Target.Start, Mode=TwoWay}" EndTime="{Binding Target.End, Mode=TwoWay}"/>-->
</Grid>
</ControlTemplate>
</ContentControl.Resources>
<Grid x:Name="Layout" Background="Aquamarine">
<ItemsControl x:Name="PlaceHolder" Template="{StaticResource DisplayTemplate}">
</ItemsControl>
</Grid>
</ContentControl>
Code Behind
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace Splan_RiaBusinessApplication.Controls
{
public class TimeCode
{
public int TimeCodeId {get;set;}
public string Code { get; set; }
public string Description { get; set; }
}
public class TimeDetail
{
public int TimeDetailId { get;set; }
public int CodeId { get;set;}
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
public string Code { get; set; }
public string Comment { get; set; }
}
public partial class TimeCodeControl : ContentControl
{
public class TimeCodeControlEventArgs : EventArgs
{
public string userName { get; set; }
}
private static TimeSpan DoubleClickThreshold = TimeSpan.FromMilliseconds(300);
private DateTime _lastClick;
private Boolean m_EditMode = false;
public Boolean EditMode
{
get { return m_EditMode; }
set
{
if (m_EditMode != value)
{
switch (value)
{
case false:
PlaceHolder.Template = this.Resources["DisplayTemplate"] as ControlTemplate;
break;
case true:
PlaceHolder.Template = this.Resources["EditTemplate"] as ControlTemplate;
break;
}
m_EditMode = value;
}
}
}
public bool IsFocused
{
get
{
return FocusManager.GetFocusedElement() == this;
}
}
public TimeCodeControl()
{
TimeCodes = new List<TimeCode>() { new TimeCode { TimeCodeId = 200, Code= "C", Description="Cash" } };
InitializeComponent();
Layout.DataContext = this;
this.IsTabStop = true;
this.Visibility = Visibility.Visible;
this.IsEnabled = true;
this.Focus();
Layout.MouseLeftButtonDown += Layout_MouseLeftButtonDown;
//Layout.KeyDown += Layout_KeyDown;
//Layout.KeyUp += Layout_KeyUp;
this.LostFocus += TimeCodeControl_LostFocus;
this.GotFocus += TimeCodeControl_GotFocus;
}
void TimeCodeControl_GotFocus(object sender, RoutedEventArgs e)
{
}
void TimeCodeControl_LostFocus(object sender, RoutedEventArgs e)
{
}
public TimeDetail Source
{
get { return (TimeDetail)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(TimeDetail), typeof(TimeCodeControl),
new PropertyMetadata(SourceChanged));
private static void SourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var control = sender as TimeCodeControl;
if (control == null) return;
control.Target = control.Source; //.Copy();
}
public List<TimeCode> TimeCodes { get; set; }
public TimeDetail Target { get; set; }
private bool FocusIsInside(object parent)
{
bool rs = false;
dynamic oFocus = FocusManager.GetFocusedElement();
while (oFocus != null)
try
{
if ((oFocus.GetType() == parent.GetType()) && (oFocus.Equals(this)))
{
rs = true;
break;
}
else
{
oFocus = oFocus.Parent;
}
}
catch
{
break;
}
return rs;
}
private Boolean hasFocus = false;
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (!hasFocus)
{
hasFocus = true;
Debug.WriteLine("Container Got Focus");
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
//if (!FocusIsInside(Layout))
//{
// hasFocus = false;
// Debug.WriteLine("Container Lost Focus");
// EditMode = false;
//}
}
void Layout_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (DateTime.Now - this._lastClick <= DoubleClickThreshold)
{
EditMode = true;
this._lastClick = DateTime.Now;
e.Handled = true;
return;
}
this._lastClick = DateTime.Now;
}
}
}
UPDATE :
I decided to utilize timers to identify a scenario when user brings a focus from outside of the container or just switches focus from one control to another inside of the container. it may be not the best solution but it seems to be working for now. I would appreciate any suggestions or recommendations on different approaches or implementations.
public partial class MyControl: ContentControl
{
...
public event EventHandler<RoutedEventArgs> LostFocus;
public event EventHandler<RoutedEventArgs> GotFocus;
bool Focused = false;
DispatcherTimer FocusTimer = null;
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
if (Focused) return;
Focused = true;
// it focused from the outside of the control
// becouse the timer wasn't initialised on the previous LostFocused event
// generated by other control in the same ContentControl contaner
if (FocusTimer == null)
{
if (GotFocus != null)
GotFocus(e.OriginalSource, e);
Debug.WriteLine("Got Focus ");
return;
}
// It was switched from one hosted control to another one
FocusTimer.Stop();
FocusTimer = null;
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
if (e.OriginalSource is ComboBox && FocusManager.GetFocusedElement() is ComboBoxItem)
return;
FocusTimer = new DispatcherTimer();
Focused = false;
FocusTimer.Interval = new TimeSpan(0, 0, 0, 0, 50);
FocusTimer.Tick += (s, args) =>
{
FocusTimer.Stop();
FocusTimer = null;
// if after the timeout the focus still not triggered
// by another contained element
// the We lost a focus on the container
if (!Focused )
{
if(LostFocus != null)
LostFocus(e.OriginalSource, e);
Debug.WriteLine("Lost Focus " );
}
};
FocusTimer.Start();
}
...
}
There are several issues. Let's see...
Why are you not getting a LostFocus event when you click outside of the control?
Well, I fell victim to this false assumption some time ago too. The click outside does not change the focus unless you click a control that explicitly sets focus to itself on click (like a TextBox does, or the various Buttons).
Press Tab to navigate the keyboard focus to the next control and see if the event is raised.
But let's talk about the other issues:
ControlTemplate x:Key="DisplayTemplate" and ControlTemplate x:Key="EditTemplate"
Using ControlTemplates this way is not recommended. Rather use DataTemplate and corresponding ContentPresenters.
TimeCodeControl : ContentControl and x:Class="Splan_RiaBusinessApplication.Controls.TimeCodeControl"
Yes I know that's possible, but not really useful. Let me explain:
You can write your own specialized Click-To-Edit Control as a one-shot tool: having a hardcoded DisplayTemplate and EditTemplate to edit TimeCode and TimeDetail data (basically what you did). But then you have no chance of ever using it and specifying another pair of Templates to allow editing of other data types.
So it doesn't make much sense to derive from ContentControl, you could as well derive from UserControl.
An alternative would be: Write your Click-To-Edit control as a general and reusable control, that offers two public properties: DisplayTemplate and EditTemplate. And don't make any assumptions about your DataContext. And again there is no benefit from having ContentControl as the parent class.
I recommend you derive from Control, add two DependencyProperties of type DataTemplate as mentioned earlier, define a default ControlTemplate with one or two ContentPresenters inside. In your control code you need to handle MouseLeftButtonDown and LostFocus and update a boolean flag accordingly.
Here is a working example:
...extension method to determine focus:
public static class ControlExtensions
{
public static bool IsFocused( this UIElement control )
{
DependencyObject parent;
for (DependencyObject potentialSubControl = FocusManager.GetFocusedElement() as DependencyObject; potentialSubControl != null; potentialSubControl = parent)
{
if (object.ReferenceEquals( potentialSubControl, control ))
{
return true;
}
parent = VisualTreeHelper.GetParent( potentialSubControl );
if (parent == null)
{
FrameworkElement element = potentialSubControl as FrameworkElement;
if (element != null)
{
parent = element.Parent;
}
}
}
return false;
}
}
...and a nice custom control:
public class ClickToEditControl : Control
{
public ClickToEditControl()
{
DefaultStyleKey = typeof (ClickToEditControl);
MouseLeftButtonDown += OnMouseLeftButtonDown;
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount==2)
{
GotoEditMode();
e.Handled = true;
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
if (!this.IsFocused())
GotoDisplayMode();
}
private void GotoDisplayMode()
{
IsInEditMode = false;
}
private void GotoEditMode()
{
IsInEditMode = true;
}
public DataTemplate EditTemplate
{
get { return (DataTemplate) GetValue( EditTemplateProperty ); }
set { SetValue( EditTemplateProperty, value ); }
}
public static readonly DependencyProperty EditTemplateProperty =
DependencyProperty.Register( "EditTemplate", typeof( DataTemplate ), typeof( ClickToEditControl ), null );
public DataTemplate DisplayTemplate
{
get { return (DataTemplate) GetValue( DisplayTemplateProperty ); }
set { SetValue( DisplayTemplateProperty, value ); }
}
public static readonly DependencyProperty DisplayTemplateProperty =
DependencyProperty.Register( "DisplayTemplate", typeof( DataTemplate ), typeof( ClickToEditControl ), null );
public bool IsInEditMode
{
get { return (bool) GetValue( IsInEditModeProperty ); }
set { SetValue( IsInEditModeProperty, value ); }
}
public static readonly DependencyProperty IsInEditModeProperty =
DependencyProperty.Register( "IsInEditMode", typeof( bool ), typeof( ClickToEditControl ), null );
}
...and ControlTemplate:
<clickToEdit:BoolToVisibilityConverter x:Key="VisibleIfInEditMode"/>
<clickToEdit:BoolToVisibilityConverter x:Key="CollapsedIfInEditMode" VisibleIfTrue="False"/>
<Style TargetType="clickToEdit:ClickToEditControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="clickToEdit:ClickToEditControl">
<Grid>
<ContentPresenter
ContentTemplate="{TemplateBinding EditTemplate}"
Content="{Binding}"
Visibility="{Binding IsInEditMode, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource VisibleIfInEditMode}}"/>
<ContentPresenter
ContentTemplate="{TemplateBinding DisplayTemplate}"
Content="{Binding}"
Visibility="{Binding IsInEditMode, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CollapsedIfInEditMode}}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
BoolToVisibilityConverter
public class BoolToVisibilityConverter : IValueConverter
{
public bool VisibleIfTrue { get; set; }
public BoolToVisibilityConverter(){VisibleIfTrue = true;}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (VisibleIfTrue)
return ((bool) value) ? Visibility.Visible : Visibility.Collapsed;
else
return ((bool) value) ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){throw new NotSupportedException();}
}
Usage:
<clickToEdit:ClickToEditControl Height="20" Width="200">
<clickToEdit:ClickToEditControl.DisplayTemplate>
<DataTemplate>
<TextBlock Text="{Binding MyText}"/>
</DataTemplate>
</clickToEdit:ClickToEditControl.DisplayTemplate>
<clickToEdit:ClickToEditControl.EditTemplate>
<DataTemplate>
<TextBox Text="{Binding MyText, Mode=TwoWay}"/>
</DataTemplate>
</clickToEdit:ClickToEditControl.EditTemplate>
</clickToEdit:ClickToEditControl>
Related
I'm trying to bind color of Gauge fill property to SolidColorBrush i created code behind. It works fine for progressbar but not for Gauge. The binding works but it doesn't update automatically as same as progressbar. Is that possible to update it while not having to re-plot this? Thank you
XAML:
<Grid Grid.Column="0">
<lvc:Gauge Grid.Row="2" Grid.Column="0" Margin="5"
From="0" To="{Binding Path=attaBitrateUP}" Value="{Binding Path=actBitrateUP}" GaugeActiveFill="{Binding Path=up, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<Grid Grid.Row="2">
<StackPanel>
<TextBlock Text="Carrier count:" Height="20" VerticalAlignment="Bottom" Margin="10,0,0,5"/>
<ProgressBar Height="25" Margin="10,0,10,0" Value="{Binding Path=chartValuesUP}" Maximum="{Binding Path=chartValuesCount}" Foreground="{Binding Path=up}" Background="{Binding Path=down}"/>
</StackPanel>
</Grid>
Code Behind:
private SolidColorBrush _up = new SolidColorBrush(Colors.OrangeRed);
public SolidColorBrush up
{
get { return _up; }
set
{
_up = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
The property GaugeActiveFill has no property changed callback registered. Therefore, it won't detect the change when the data binding is invoked. The library is implemented quite sloppy, frankly said.
But we can fix it ourselves by extending the original Gauge class to register the missing property changed handler:
FixedGauge.cs
public class FixedGauge : Gauge
{
static FixedGauge()
{
GaugeActiveFillProperty.OverrideMetadata(typeof(FixedGauge), new FrameworkPropertyMetadata(default(Brush), OnGaugeActiveFillChanged));
}
private PieSlice PART_Pie { get; set; }
public FixedGauge()
{
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (TryFindVisualChildElementByName(this, null, out PieSlice pie))
{
this.PART_Pie = pie;
}
}
private static void OnGaugeActiveFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
=> (d as FixedGauge).OnGaugeActiveFillChanged(e.OldValue as Brush, e.NewValue as Brush);
protected virtual void OnGaugeActiveFillChanged(Brush oldBrush, Brush newBrush)
{
if (this.PART_Pie == null)
{
return;
}
this.PART_Pie.Fill = newBrush;
}
public static bool TryFindVisualChildElementByName<TChild>(
DependencyObject parent,
string childElementName,
out TChild resultElement) where TChild : FrameworkElement
{
resultElement = null;
if (parent is Popup popup)
{
parent = popup.Child;
if (parent == null)
{
return false;
}
}
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is TChild frameworkElement)
{
if (string.IsNullOrWhiteSpace(childElementName) || frameworkElement.Name.Equals(
childElementName,
StringComparison.OrdinalIgnoreCase))
{
resultElement = frameworkElement;
return true;
}
}
if (TryFindVisualChildElementByName(childElement, childElementName, out resultElement))
{
return true;
}
}
return false;
}
}
Then use this new control in place of the original:
<local:FixedGauge Grid.Row="2" Grid.Column="0" Margin="5"
From="0" To="{Binding Path=attaBitrateUP}"
Value="{Binding Path=actBitrateUP}"
GaugeActiveFill="{Binding Path=up}" />
I am currently developing a hamburger style menu in WPF. In this menu, there are some categories that each have an icon. When the menu is collapsed you can still see those icons. When you expand the menu, there should appear text next to it. My idea was to just set their visibility to Visible as soon as the menu opens but I've had a lot of trouble realizing this. Right now I'm trying to change their visibility by binding them to a property.
XAML:
<ListView x:Name="menuItemsListView" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListViewItem>
<StackPanel Orientation="Horizontal">
<Image x:Uid="Test" Name="InhoudImage" Source="Images/noimage.png" Height="30" Width="auto" VerticalAlignment="Center" Margin="3,0,0,0"></Image>
<TextBlock x:Uid="Test" Text="{Binding Path=TextboxVisibility}" Visibility="{Binding Path=TextboxVisibility}" VerticalAlignment="Center"></TextBlock>
</StackPanel>
</ListViewItem>
</ListView>
C# CS Class:
using System.Windows;
using System.Windows.Controls;
namespace APP
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private bool menuOpen = false;
private int closedMenuWidth = 50;
private int openMenuWidth = 210;
private string textboxVisibility;
public string TextboxVisibility
{
get { return textboxVisibility; }
set { textboxVisibility = value; }
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.TextboxVisibility = "Hidden";
}
private void MenuButton_Click(object sender, RoutedEventArgs e)
{
if (menuOpen)
{
menuGrid.Width = closedMenuWidth;
menuOpen = false;
this.TextboxVisibility = "Hidden";
}
else
{
menuGrid.Width = openMenuWidth;
menuOpen = true;
this.TextboxVisibility = "Visible";
//foreach (ListViewItem item in menuItemsListView.Items)
//{
// item.
// if (item.Uid == "Test")
// {
// item.Visibility = Visibility.Visible;
// }
//}
}
}
}
}
When I change the value within the MainWindow function, it does have an effect on it when it first starts. But the other times I try to change it, which is at runtime, nothing happens. I have tried all sorts of things with booleans and binding the actual Visibility type but nothing worked.
You should implemente INotifyPropertyChanged on your MainWindow class like this:
public partial class MainWindow: Window,INotifyPropertyChanged {
private string textboxVisibility;
public string TextboxVisibility {
get {
return textboxVisibility;
}
set {
textboxVisibility = value;
OnPropertyChanged();
}
}
//The rest of your code goes here
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged ? .Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
What OnPropertyChanged method does is, whenever the value is setted, it notifies the view and refreshes it.
This will solve the problem but isn't the right way to use MVVM.
The way you should do this is to change the visibility property of the TextBox instead of binding the visibility property to a value:
First you have to add a name to the TextBlock you want to hide:
<ListView x:Name="menuItemsListView" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListViewItem>
<StackPanel Orientation="Horizontal">
<Image x:Uid="Test" Name="InhoudImage" Source="Images/noimage.png" Height="30" Width="auto" VerticalAlignment="Center" Margin="3,0,0,0"></Image>
<TextBlock Name="textblock" x:Uid="Test" Text="{Binding Path=TextboxVisibility}" Visibility="{Binding Path=TextboxVisibility}" VerticalAlignment="Center"></TextBlock>
</StackPanel>
</ListViewItem>
</ListView>
And then you change the visibility in the code
private void MenuButton_Click(object sender, RoutedEventArgs e) {
if (menuOpen) {
menuGrid.Width = closedMenuWidth;
menuOpen = false;
textblock.Visibility = System.Windows.Visibility.Hidden;
}
else {
menuGrid.Width = openMenuWidth;
menuOpen = true;
textblock.Visibility = System.Windows.Visibility.Visible;
//foreach (ListViewItem item in menuItemsListView.Items)
//{
// item.
// if (item.Uid == "Test")
// {
// item.Visibility = Visibility.Visible;
// }
//}
}
}
If you want to implement MVVM the right way you have to create a ViewModel class and add it as Data Context to your view:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
And then on you MainWindowViewModel is where you change the property:
public class MainWindowViewModel: INotifyPropertyChanged {
private string textboxVisibility;
public string TextboxVisibility {
get {
return textboxVisibility;
}
set {
textboxVisibility = value;
OnPropertyChanged();
}
}
//The rest of your code goes here
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChanged ? .Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I have a child control within a form which shows the strength of the WiFi signal As this application is to be used on a tablet whithin a moving vehicle round the site. I would like to have this control within every form of my WPF application and I would like it to refresh every second without having to change the code behind each form it is on.
It is possible to refresh this control using the parent form's code behind by calling signalQualityView.Refresh(); every second but would like this functionality to be implemented within the SignalQualityView usercontrol .
public partial class SignalQualityView : UserControl, INotifyPropertyChanged
{
// wifi signal indicator taken from from https://stackoverflow.com/questions/20085284/c-sharp-wpf-rating-control-similar-to-wifi-signal-indicator
private NetworkInformationService _networkInformationService;
public SignalQualityView()
{
InitializeComponent();
DataContext = this;
_networkInformationService = new NetworkInformationService();
Refresh();
}
public void Refresh()
{
Task.Run(() =>
{
WirelessNetwork wirelessNetwork = _networkInformationService.GetWirelessNetworkDetails();
var signalQuality = wirelessNetwork.SignalQuality;
if (signalQuality >= 80)
RatingValue = 5;
else if (signalQuality >= 60)
RatingValue = 4;
else if (signalQuality >= 40)
RatingValue = 3;
else if (signalQuality >= 20)
RatingValue = 2;
else if (signalQuality >= 1)
RatingValue = 1;
else
RatingValue = 0;
Task.Delay(1000);
Refresh();
});
}
public int RatingValue
{
get { return (int)GetValue(RatingValueProperty); }
set
{
SetValue(RatingValueProperty, value);
OnPropertyChanged();
}
}
// Using a DependencyProperty as the backing store for RatingValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RatingValueProperty =
DependencyProperty.Register("RatingValue", typeof(int), typeof(SignalQualityView), new UIPropertyMetadata(0));
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class RatingConverter : IValueConverter
{
public Brush OnBrush { get; set; }
public Brush OffBrush { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
int rating = 0;
int number = 0;
if (int.TryParse(value.ToString(), out rating) && int.TryParse(parameter.ToString(), out number))
{
if (rating >= number)
{
return OnBrush;
}
return OffBrush;
}
return Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
<UserControl x:Class="App.PlugIn.Controls.SignalQualityView"
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:DPWorld.PlugIn.Controls"
mc:Ignorable="d"
Height="40" Width="60">
<Grid Background="black" >
<Grid.Resources>
<local:RatingConverter x:Key="RatingConverter" OnBrush="LightBlue" OffBrush="Black" />
<Style TargetType="Rectangle">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="Margin" Value="5,0,0,0" />
</Style>
</Grid.Resources>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Background="Black" VerticalAlignment="Center">
<Rectangle Width="5" Height="5" Fill="{Binding RatingValue, Converter={StaticResource RatingConverter}, ConverterParameter=1}"/>
<Rectangle Width="5" Height="10" Fill="{Binding RatingValue, Converter={StaticResource RatingConverter}, ConverterParameter=2}"/>
<Rectangle Width="5" Height="15" Fill="{Binding RatingValue, Converter={StaticResource RatingConverter}, ConverterParameter=3}"/>
<Rectangle Width="5" Height="20" Fill="{Binding RatingValue, Converter={StaticResource RatingConverter}, ConverterParameter=4}"/>
<Rectangle Width="5" Height="25" Fill="{Binding RatingValue, Converter={StaticResource RatingConverter}, ConverterParameter=5}"/>
</StackPanel>
<Label Content="SIGNAL" Foreground="LightBlue" VerticalAlignment="Top" Height="15" FontSize="8" Margin="10,0,18,0" Padding="0,0,0,0"/>
</Grid>
</UserControl>
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pi="clr-namespace:App.PlugIn.Controls;assembly=PlugIn"
Title="Window1" Height="300" Width="300">
<Grid>
<pi:SignalQualityView Name="signalQualityView" />
</Grid>
</Window>
The number of signal bars are meant highlighted depending on the signal strength but 0 bars are highlighted.
A simple UserControl that updates itself periodically would use a DispatcherTimer with a Tick event handler that updates a dependency property of the control. An element in the control's XAML would bind the control property by a RelativeSource Binding.
A simple digital clock as example:
<UserControl ...>
<Grid>
<TextBlock Text="{Binding Time,
RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Grid>
</UserControl>
Code behind:
public partial class Clock : UserControl
{
public Clock()
{
InitializeComponent();
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(0.5) };
timer.Tick += (s, e) => Time = DateTime.Now.ToString("HH:mm:ss");
timer.Start();
}
public static readonly DependencyProperty TimeProperty =
DependencyProperty.Register(nameof(Time), typeof(string), typeof(Clock));
public string Time
{
get { return (string)GetValue(TimeProperty); }
set { SetValue(TimeProperty, value); }
}
}
When using WPF, data on a window that changes should come from an IObservable property of the bound object. When the property changes, the display value changes.
Put the current value into a property of a shared object using a task that runs every second. Share the object across all bound objects and no code behind is required.
You start of with a class that can be shared and observed:
public class WiFiStrength : IObservable<int>
{
private int _signalQuality;
private int _ratingValue;
private class Unsubscriber : IDisposable
{
private List<IObserver<int>> _observers;
private readonly IObserver<int> _observer;
public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers != null)
{
_observers.Remove(_observer);
_observers = null;
}
}
}
private List<IObserver<int>> _observers = new List<IObserver<int>>();
private void SetAndRaiseIfChanged(int newRating)
{
if (_ratingValue != newRating)
{
_ratingValue = newRating;
foreach (var observer in _observers)
{
observer.OnNext(newRating);
}
}
}
private static WiFiStrength _sharedInstance;
private static Task _updater;
private static CancellationTokenSource cts;
private static void UpdateStrength(WiFiStrength model, CancellationToken ct)
{
var rnd = new Random();
while (!ct.IsCancellationRequested)
{
model.SignalQuality = rnd.Next(100);
Thread.Sleep(1000);
}
}
public static WiFiStrength SharedInstance()
{
if (_sharedInstance == null)
{
_sharedInstance = new WiFiStrength();
cts = new CancellationTokenSource();
_updater = new Task(() => UpdateStrength(_sharedInstance, cts.Token));
_updater.Start();
}
return _sharedInstance;
}
public int SignalQuality
{
get => _signalQuality;
set
{
_signalQuality = value;
if (_signalQuality >= 80)
{
SetAndRaiseIfChanged(5);
}
else if (_signalQuality >= 60)
{
SetAndRaiseIfChanged(4);
}
else if (_signalQuality >= 40)
{
SetAndRaiseIfChanged(3);
}
else if (_signalQuality >= 20)
{
SetAndRaiseIfChanged(2);
}
else if (_signalQuality >= 1)
{
SetAndRaiseIfChanged(1);
}
else
{
SetAndRaiseIfChanged(0);
}
}
}
public int SignalRating => _ratingValue;
public IDisposable Subscribe(IObserver<int> observer)
{
_observers.Add(observer);
return new Unsubscriber(_observers, observer);
}
}
You attach it to your bound class:
public class ViewModel1 : INotifyPropertyChanged
{
public string TextField { get; set; }
public WiFiStrength WiFi { get; set; }
private IObserver<int> _observer;
private class WiFiObserver : IObserver<int>
{
private readonly ViewModel1 _parent;
private readonly IDisposable _unsubscribe;
public WiFiObserver(ViewModel1 parent, WiFiStrength observed)
{
_parent = parent;
_unsubscribe = parent.WiFi.Subscribe(this);
}
public void OnNext(int value)
{
_parent.NotifyPropertyChanged("WiFi");
}
public void OnError(Exception error)
{
_unsubscribe.Dispose();
}
public void OnCompleted()
{
_unsubscribe.Dispose();
}
}
public ViewModel1()
{
WiFi = WiFiStrength.SharedInstance();
_observer = new WiFiObserver(this, WiFi);
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Your class looks for changes, and notifies the owning form if it does change.
Your XAML need only bind to the property like this:
<Label Content="{Binding WiFi.SignalRating, Mode=OneWay}"/>
Use on as many forms as you like.
what I am trying to do is somewhat out there, and I have yet to really see an example of this.
I am trying to validate a textbox entry that is essentially a required field (it cannot be null or empty). However, I do not have any access to the code behind, only to the XAML and data binding for the form.
From searching for a couple of days, I found out this cannot be done strictly in XAML (which would have been preferred), and had to create my own resource library to check for this. That is what I have done, but failed to get it to work.
Is this even a possibility? Or what would I have to do to get this to work?
What I have done so far was create a usercontrol template of a textbox to then use in the XAML (residing in an outside library):
<UserControl.Resources>
<Style x:Key="TextBoxStyle" TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder Name="MyAdorner"/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<DockPanel x:Name="dpMain" LastChildFill="True">
<Label/>
</DockPanel>
</Grid>
And the code behind:
namespace ClassLibrary.CustomControls
{
public partial class CssTextBox : UserControl
{
private TextBox _textbox = null;
private ObservableCollection<ValidationRule> _validationRules = null;
public CssTextBox()
{
InitializeComponent();
CreateControls();
ValidationRules = new ObservableCollection<ValidationRule>();
this.DataContextChanged += new DependencyPropertyChangedEventHandler(CssTextBoxDataChanged);
}
public ObservableCollection<ValidationRule> ValidationRules
{
get { return _validationRules; }
set { _validationRules = value; }
}
private void CreateControls()
{
_textbox = new TextBox() { Width = 100, Height = 20 };
_textbox.LostFocus += CssTextBoxLostFocus;
_textbox.Style = TextBoxErrorStyle;
}
public void CssTextBoxDataChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (_textbox != null)
{
var binding = new Binding();
binding.Source = this.DataContext;
binding.ValidatesOnDataErrors = true;
binding.ValidatesOnExceptions = true;
foreach (var rule in ValidationRules)
{
binding.ValidationRules.Add(rule);
}
binding.Path = new PropertyPath(BoundPropertyName);
_textbox.SetBinding(TextBox.TextProperty, binding);
dpMain.Children.Add(_textbox);
}
}
public void CssTextBoxLostFocus(object sender, RoutedEventArgs e)
{
var bindingExpression = _textbox.GetBindingExpression(TextBox.TextProperty);
if (bindingExpression != null)
bindingExpression.UpdateSource();
}
private Style TextBoxErrorStyle
{
get
{
return (Style)FindResource("TextBoxStyle");
}
}
public string TextBoxErrorStyleName { get; set; }
public string BoundPropertyName { get; set; }
public string ValidationExpression { get; set; }
public string Text
{
get
{
return _textbox.Text;
}
}
public string ErrorText { get; set; }
}
And how it is being used (currently being tested in a WPF Sandbox project and only being referenced via XAML):
xmlns:css="clr-namespace:WpfSandbox.CustomControls" <!--Reference to library that holds above--!>
<css:CssTextBox TextBoxErrorStyleName="TextBoxStyle" Grid.Column="0" Grid.Row="1" Width="100" Height="20" VerticalAlignment="Top" >
<css:CssTextBox.ValidationRules>
<validators:NotNullOrEmptyValidationRule ErrorMessage="Cannot be Empty!" />
</css:CssTextBox.ValidationRules>
</css:CssTextBox>
<TextBox Grid.Column="0" Grid.Row="2" Width="auto" Height="20" VerticalAlignment="Top" Background="White" IsEnabled="True"/>
My issue with what I have now, is that it shows the textbox in my designer window in my sandbox application, but I cannot click into it when I run. It's almost like it does not exist.
Thanks for any insight!
You should read about WPF Data validation.
This link will help you:
https://msdn.microsoft.com/fr-fr/library/system.componentmodel.idataerrorinfo(v=vs.95).aspx
So let's say I have a simple little WPF app that looks like this:
-------------------------------
Amount Left: 1,000
[Subtract 1]
[Subtract 5]
[Subtract 15]
[Subtract 30]
-------------------------------
"Amount Left:" and "1,000" are stand alone TextBlocks.
The "Subtract x" are all buttons, inside a ListView, inside a DataTemplate. Each time a button is clicked, the amount of the button is subtracted from the 1,000. All of that I have working.
Here's what I can't figure out. When the Amount Left falls below 30, the last button needs to become disabled. When the amount falls below 15, the second to last button becomes disabled. Etc and so on, until the Amount Left is Zero and all buttons are disabled. I can not figure out how to disable the buttons.
This example I'm giving here is not exactly what I'm trying to do, but it's a greatly simplified example that will make this post a lot shorter and simpler. Here, in essence, is what I have now.
XAML:
<DockPanel>
<TextBlock Text="Amount Left:" />
<TextBlock x:Name="AmountLeft" Text="1,000.00" />
</DockPanel>
<DockPanel>
<ListBox x:Name="AuthorListBox">
<ListView.ItemTemplate>
<DataTemplate>
<Button x:Name="SubButtom" Content="{Binding SubtractAmount}" Click="clickSubtract" />
<DataTemplate>
</ListBox>
</DockPanel>
XAML.cs
private void clickSubtract(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
Int32 SubtractAmount = ((Data.AppInformation)button.DataContext).SubtractAmount; // This is the amount to be subtracted
// logic to update the amount remaining. This works.
// What I need to figure out is how to disable the buttons
}
You can accomplish using MVVM, by having an IsEnabled property for your Button ViewModels. With this approach, you will not need any 'code behind' as you currently have using a click event handler.
Xaml:
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Amount Left:" />
<TextBlock Text="{Binding CurrentAmount}" />
</StackPanel>
<ListBox ItemsSource="{Binding Buttons}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Command="{Binding SubtractCommand}" Width="200" Height="75" x:Name="SubButtom" Content="{Binding SubtractAmount}" IsEnabled="{Binding IsEnabled}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
Xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
We want the main ViewModel that will have a list of Button ViewModels.
ButtonViewModel.cs:
namespace WpfApplication1
{
public class ButtonViewModel : INotifyPropertyChanged
{
private bool _isEnabled;
private ViewModel _viewModel;
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
OnPropertyChanged();
}
}
public int SubtractAmount { get; set; }
public ICommand SubtractCommand { get; private set; }
public ButtonViewModel(ViewModel viewModel)
{
_viewModel = viewModel;
IsEnabled = true;
SubtractCommand = new CommandHandler(() =>
{
_viewModel.CurrentAmount -= SubtractAmount;
}, true);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class CommandHandler : ICommand
{
private readonly Action _action;
private readonly bool _canExecute;
public CommandHandler(Action action, bool canExecute)
{
_action = action;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_action();
}
}
}
and now the main ViewModel.
ViewModel.cs:
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
private int _currentAmount;
public int CurrentAmount
{
get { return _currentAmount; }
set
{
_currentAmount = value;
OnPropertyChanged();
if (Buttons != null)
{
foreach (var button in Buttons)
{
if ((value - button.SubtractAmount) <= 0)
{
button.IsEnabled = false;
}
}
}
}
}
public List<ButtonViewModel> Buttons { get; private set; }
public ViewModel()
{
CurrentAmount = 1000;
Buttons = new List<ButtonViewModel>
{
new ButtonViewModel(this)
{
SubtractAmount = 1
},
new ButtonViewModel(this)
{
SubtractAmount = 5
},
new ButtonViewModel(this)
{
SubtractAmount = 15
},
new ButtonViewModel(this)
{
SubtractAmount = 30
}
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
As you can see, each Button ViewModel will decrement the CurrentAmount using a Command (the preferred method over a click event). Whenever the CurrentAmount is changed, some simple logic is done by the main ViewModel that will disable associated buttons.
This is tested and works. Let me know if you have any questions.
I would go ahead creating a Converter and bind the IsEnabled property of the button. pass the value and do the logic.
Namespace
System.Windows.Data
System.Globalization
CODE
public class IsEnabledConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
// Do the logic
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
// Do the Logic
}
}
XAML
Add the resurce like this
<Window.Resources>
<local:IsEnabledConverter x:Key="converter" />
</Window.Resources>
<Button x:Name="SubButtom" IsEnabled="{Binding Value, Converter= {StaticResource converter}}" Content="{Binding SubtractAmount}" Click="clickSubtract" />
You can learn about converters from below link
http://wpftutorial.net/ValueConverters.html
When you build the class with Converter all Xaml Errors will go off.
Your best option would be to use a command on the viewmodel instead of a click event handler:
public ICommand SubtractCommand = new DelegateCommand<int>(Subtract, i => i <= AmountLeft);
private void Subtract(int amount)
{
AmountLeft = AmountLeft - amount;
}
XAML:
<ListBox x:Name="AuthorListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<Button x:Name="SubButtom" Content="{Binding SubtractAmount}"
Command="{Binding SubtractCommand, RelativeSource={RelativeSource AncestorType=DataGrid}}"
CommandParameter="{Binding SubtractAmount}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>