Problem
A user WPF control is made up of multiple standard controls.
How can multiple dependency properties of the component (base or standard) controls be accessed in XAML, when implementing the parent (user) control, without creating additional properties?
Details
What do I mean by "creating additional dependency properties"? Well, that is the only way I know of accessing properties of the component controls: by implementing attached properties, as described at MSDN here.
However, it presents the following problems:
Existing dependency properties must be copied as new properties, defeating the DRY principle.
If data binding is to occur, more work must be done to bind existing dependency properties to the new exposed dependency properties.
I'm wondering if there is a way to "walk" the base controls within the user control, to access their properties - from within XAML.
Example
For example, I make a user WPF control that inherits from UserControl. It is simple - it consists of a StackPanel containing a Label and a TextBlock:
<UserControl x:Class="MyApp.CustomControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<Label Name="BaseLabel">Label Here</Label>
<TextBlock Name="BaseTextBlock">Some text here.</TextBlock>
</StackPanel>
</UserControl>
Now, when I use my UserControl elsewhere in XAML, I'm wishfully thinking something like this could be done to edit my Label's content... although I don't know of a way:
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp">
<StackPanel>
<!-- This won't work, don't try at home kids. -->
<local:CustomControl BaseLabel.Content="I did it!"></local:CustomControl>
</StackPanel>
</Window>
Much thanks.
How about the next solution:
1. Create the AttachedProperty (because you must an entry point) and bind this property to the collection of data.This collection of data will contain changes you want perform on sub-controls of a main user control used inside the window. This collection will be defined inside the main window view model.
2. In attached property changed callback get the binded collection, parse it data into sub-controls properties.
Here is the solution:
3. Xaml code:
<Window.DataContext>
<nirHelpingOvalButton:MainWindowViewModel />
</Window.DataContext>
<Grid>
<nirHelpingOvalButton:InnerControl x:Name="MyInnerControl"
nirHelpingOvalButton:Helper.InnerControlPropertiesAccessor="{Binding InnerData, Mode=Default, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
4. Attached property code (bindig support):
public static readonly DependencyProperty InnerControlPropertiesAccessorProperty = DependencyProperty.RegisterAttached(
"InnerControlPropertiesAccessor", typeof (ObservableCollection<TargetControlData>), typeof (Helper), new PropertyMetadata(default(ObservableCollection<TargetControlData>), InnerValueAccessProviderPropertyChangedCallback));
public static void SetInnerControlPropertiesAccessor(DependencyObject element, ObservableCollection<TargetControlData> value)
{
element.SetValue(InnerControlPropertiesAccessorProperty, value);
}
public static ObservableCollection<TargetControlData> GetInnerControlPropertiesAccessor(DependencyObject element)
{
return (ObservableCollection<TargetControlData>) element.GetValue(InnerControlPropertiesAccessorProperty);
}
private static void InnerValueAccessProviderPropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var control = sender as Control;
if (control == null) return;
var valuesMap = args.NewValue as ObservableCollection<TargetControlData>;
if (valuesMap == null)
return;
valuesMap.ToList().ForEach(data => TryToBind(control, data));
}
private static void TryToBind(Control control, TargetControlData data)
{
var innerControl = control.FindName(data.SubControlName) as DependencyObject;
if (innerControl == null) return;
var myBinding = new Binding
{
Source = data,
Path = new PropertyPath("Data"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
var descriptors = TypeDescriptor.GetProperties(innerControl);
var propertyDescriptor = descriptors.Find(data.SubConrolProperty, true);
var descriptor = DependencyPropertyDescriptor.FromProperty(propertyDescriptor);
if (descriptor == null) return;
var dependencyProperty = descriptor.DependencyProperty;
BindingOperations.SetBinding(innerControl, dependencyProperty, myBinding);
}
5. Inner control xaml:
<UserControl x:Class="NirHelpingOvalButton.InnerControl"
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">
<UniformGrid>
<Button x:Name="InnerControlButton"></Button>
<TextBlock x:Name="InnerContentTextBlock"></TextBlock>
</UniformGrid>
6. ViewModel code:
public class MainWindowViewModel:BaseObservableObject
{
private static int _staticCount = 0;
private List<Brush> _list = new List<Brush> {Brushes.Green, Brushes.Red, Brushes.Blue};
public MainWindowViewModel()
{
InnerData = new ObservableCollection<TargetControlData>
{
new TargetControlData
{
SubControlName = "InnerControlButton",
SubConrolProperty = "Content",
Data = "Click Me",
},
new TargetControlData
{
SubControlName = "InnerControlButton",
SubConrolProperty = "Command",
Data = new RelayCommand(CommandMethod),
},
new TargetControlData
{
SubConrolProperty = "Text",
SubControlName = "InnerContentTextBlock",
Data = "Hello"
},
new TargetControlData
{
SubConrolProperty = "Background",
SubControlName = "InnerContentTextBlock",
Data = Brushes.Green
},
new TargetControlData
{
SubConrolProperty = "Foreground",
SubControlName = "InnerContentTextBlock",
Data = Brushes.White
},
};
}
private void CommandMethod()
{
_staticCount ++;
var backgroundData = InnerData.FirstOrDefault(data => data.SubControlName == "InnerContentTextBlock" && data.SubConrolProperty == "Background");
var textData = InnerData.FirstOrDefault(data => data.SubControlName == "InnerContentTextBlock" && data.SubConrolProperty == "Text");
if (backgroundData == null || textData == null) return;
var index = _staticCount%_list.Count;
backgroundData.Data = _list[index];
textData.Data = string.Format("{0} {1}", "Hello", backgroundData.Data);
}
public ObservableCollection<TargetControlData> InnerData { get; set; }}
7. TargetControlData code:
public class TargetControlData:BaseObservableObject
{
private string _subControlName;
private string _subConrolProperty;
private object _data;
public string SubControlName
{
get { return _subControlName; }
set
{
_subControlName = value;
OnPropertyChanged();
}
}
public string SubConrolProperty
{
get { return _subConrolProperty; }
set
{
_subConrolProperty = value;
OnPropertyChanged();
}
}
public object Data
{
get { return _data; }
set
{
_data = value;
OnPropertyChanged();
}
}
}
Summary - you can pull control properties data from configuration file, or collect them by reflection.
regards,
The way you suggested - I don't think this would be possible.
But it can be done with normal properties, instead of dependency properties, something like:
UserControl xaml:
<StackPanel>
<TextBlock x:Name="tbOne"></TextBlock>
<TextBlock x:Name="tbTwo" Foreground="Red"></TextBlock>
</StackPanel>
UserControl code behind:
public string One
{
get
{
return this.tbOne.Text;
}
set
{
this.tbOne.Text = value;
}
}
public string Two
{
get
{
return this.tbTwo.Text;
}
set
{
this.tbTwo.Text = value;
}
}
and the usage of user control:
<local:UserControl1 One="test1" Two="test2"></local:UserControl1>
Related
I would like to implement a DataGrid that displays data of objects with the same class. A list, which accepts generic type of class, of objects called objectsList and a list of string of properties called propertiesToDisplay are provided so that the DataGrid can choose which properties of the objects to be shown, according to propertiesToDisplay. How can I implement it?
MainWindow
<Window x:Class="SomeProject.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:SomeProject"
mc:Ignorable="d"
Title="MainWindow" MinHeight="450" MinWidth="700">
<Grid>
<DataGrid ItemsSource="{Binding ObjectsList}">
<!-- Anything Else? -->
</DataGrid>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// How is the binding?
}
}
ViewModel
public class ViewModel<T>
{
private IList<T> _objectsList;
public IList<T> ObjectsList
{
get
{
return _objectsList;
}
set
{
_objectsList = value;
}
}
private IList<string> _propertiesToDisplay;
public IList<string> PropertiesToDisplay
{
get
{
return _propertiesToDisplay;
}
set
{
_propertiesToDisplay= value;
}
}
// Constructor
public ViewModel(IList<T> objectsList, IList<string> propertiesToDisplay)
{
// Please help me to correct the following code
PropertyInfo[] propertyInfos = null;
foreach (var propertyName in propertiesToDisplay)
{
propertyInfos = typeof(T).GetProperties();
var names = propertyInfos.Select(x => x.Name);
if (!names.Contains(propertyName))
{
throw new ArgumentException("");
}
}
try
{
ObjectsList = objectsList;
foreach (var obj in objectsList)
{
foreach (var propertyName in propertiesToDisplay)
{
PropertiesToDisplay.Add(propertyInfos.Where(x => x.Name.Equals(propertyName)).FirstOrDefault().GetValue(obj, null).ToString());
}
}
}
catch (Exception ex)
{
}
}
}
You could create the columns dynamically in the view based on the PropertiesToDisplay source property. Something like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ViewModel<YourClass> viewModel = new ViewModel<YourClass>(..., ...);
foreach (string column in viewModel.PropertiesToDisplay)
dataGrid.Columns.Add(new DataGridTextColumn() { Header = column, Binding = new Binding(column) });
dataGrid.AutoGenerateColumns = false;
DataContext = viewModel;
}
}
XAML:
<DataGrid x:Name="dataGrid" ItemsSource="{Binding ObjectsList}" />
As you can see in code below, I have descendant of ScrollViewer, and set Name of MyScrollView in ctor. But when I try using MyScrollViewer in control template I cannot find one via Template.FindName
If I change <local:MyScrollViewer /> to <local:MyScrollViewer Name=""PART_ContentHost"" /> code works as expected, but I am looking for solution without changing XAML.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var parserContext = new ParserContext
{
XamlTypeMapper = new XamlTypeMapper(new string[0]),
XmlnsDictionary =
{
{ "", "http://schemas.microsoft.com/winfx/2006/xaml/presentation" },
{ "x", "http://schemas.microsoft.com/winfx/2006/xaml" },
{ "local", "clr-namespace:" + typeof(MyScrollViewer).Namespace + ";assembly=" + typeof(MyScrollViewer).Assembly.FullName}
}
};
var template = (ControlTemplate)XamlReader.Parse(#"
<ControlTemplate TargetType=""TextBox"">
<Border>
<local:MyScrollViewer />
<!--<local:MyScrollViewer Name=""PART_ContentHost""/> -->
</Border>
</ControlTemplate>
", parserContext);
// Name=""PART_ContentHost""
Content = new MyTextBox { Template = template };
}
}
public class MyScrollViewer: ScrollViewer
{
public MyScrollViewer() => Name = "PART_ContentHost";
}
public class MyTextBox: TextBox
{
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var contentHost = Template.FindName("PART_ContentHost", this);
if (contentHost == null)
throw new Exception("Can not find PART_ContentHost");
}
}
updated:
Even I put my control template into MainWindow.xaml (and remove from MainWindow ctor), it doesnot work.
public MainWindow()
{
InitializeComponent();
}
<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"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.Resources>
<ControlTemplate TargetType="local:MyTextBox" x:Key="TextBoxTemplate">
<local:MyScrollViewer />
</ControlTemplate>
</Grid.Resources>
<local:MyTextBox Template="{StaticResource TextBoxTemplate}" />
</Grid>
</Window>
The constructor of MyScrollViewer will not be called on template creation with XamlReader, so the names with which MyScrollViewer was created are stored in internal dictionaries. See template.ChildNames. The constructor of MyScrollViewer will first being called, when MyTextBox becomes visible, but it's already too late.
Template being created from XAML and it notices the children names by parsing, without creation of children instances. Later the children instances will be created, but the template hold old names. So if you call Template.FindNames with new names, they will not be found.
Try
var contentHost = Template.FindName("2_T", this);
but I am looking for solution without changing XAML
Setting the name in the constructor of the ScrollViewer won't work. You have to set it in the template for it to be registered in the namescope of the template.
If you don't want to assign the element a Name in the template, you could wait for it to get created and then find it in the visual tree without using a name:
public class MyTextBox : TextBox
{
public MyTextBox()
{
Loaded += MyTextBox_Loaded;
}
private void MyTextBox_Loaded(object sender, RoutedEventArgs e)
{
MyScrollViewer sv = FindVisualChild<MyScrollViewer>(this);
//...
}
private static T FindVisualChild<T>(Visual visual) where T : Visual
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
if (child != null)
{
T correctlyTyped = child as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
T descendent = FindVisualChild<T>(child);
if (descendent != null)
{
return descendent;
}
}
}
return null;
}
}
I'm trying to make a control that has a current value with an optional equation string.
I have 2 textboxes:
One (a) where you can enter an equation shortcut to a value to put into the other (b).
(b) contains the actual value.
(for example, in (a), if you enter 'pi', the second will then fill with "3.1415926535897931")
I'm using 2 textboxes so the user can refine their equation if they need to, and watch the value change as they modify it.
The data has 2 fields, one being the equation string and the other being the current value.
so I have (a).Text bound to the string, a new property on (a) that holds the value, and I bind (b).Text to the value also.
(a).Text is TwoWay
(a).Value is OneWayToSource (since changes to the text should only be pushed to b)
(b).Value is TwoWay
This all works fine if I have the data set in the constructor before any XAML binding, but does not work at all if I add the data after binding.
Here is a minimal amount of code that shows the problem.
The only comment is at the line that can make it work or not.
As a last resort I could turn it into a custom control and handle it in the code-behind, but I'd think this should work in the first place.
Any ideas why this isn't working?
Thanks!
Here is the XAML:
<Window x:Class="twoBindingsOnSameField.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:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:twoBindingsOnSameField"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<Button Content="load data" Click="Button_Click" Width="80" IsEnabled="{Binding NeedsData}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="enter text:" Width="80"/>
<local:TextBoxCalc Text="{Binding Item.ItemString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
TextBoxCalculatedValue="{Binding Item.ItemValue, Mode=OneWayToSource, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="updated text:" Width="80"/>
<TextBox Text="{Binding Item.ItemValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="200"
IsEnabled="{Binding HasData}"
/>
</StackPanel>
</StackPanel>
</Window>
Here is the codebehind.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace twoBindingsOnSameField
{
public partial class MainWindow : Window
{
data data;
public MainWindow()
{
InitializeComponent();
data = new data();
/// ---- Does not work with the following line commented out, but does if it is uncommented ----
/// ---- use the button to set the data ----
//setdata();
DataContext = data;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
setdata();
}
void setdata()
{
if (data.Item == null)
data.Item = new dataitem();
}
}
public class data : notifybase
{
dataitem item;
public data()
{
}
public dataitem Item
{
get
{
return item;
}
set
{
if (item != value)
{
item = value;
notifyPropertyChanged("Item");
notifyPropertyChanged("HasData");
notifyPropertyChanged("NeedsData");
}
}
}
public bool HasData
{
get
{
return Item != null;
}
}
public bool NeedsData
{
get
{
return Item == null;
}
}
}
public class dataitem : notifybase
{
string itemString;
string itemValue;
public dataitem()
{
itemString = "3";
itemValue = "4";
}
public virtual string ItemString
{
get
{
return this.itemString;
}
set
{
if (!object.Equals(this.itemString, value))
{
this.itemString = value;
notifyPropertyChanged("ItemString");
}
}
}
public virtual string ItemValue
{
get
{
return this.itemValue;
}
set
{
if (!object.Equals(this.itemValue, value))
{
this.itemValue = value;
notifyPropertyChanged("ItemValue");
}
}
}
}
public class TextBoxCalc : TextBox
{
public TextBoxCalc()
{
TextProperty.AddHandler(this, (o,e)=>TextBoxCalculatedValue="updated:" + Text);
}
#region TextBoxCalculatedValue
public static DependencyProperty TextBoxCalculatedValueProperty = DependencyProperty.Register("TextBoxCalculatedValue", typeof(string), typeof(TextBoxCalc), new PropertyMetadata(""));
public string TextBoxCalculatedValue
{
get
{
return (string)GetValue(TextBoxCalculatedValueProperty);
}
set
{
if (!object.Equals(TextBoxCalculatedValue, value))
SetValue(TextBoxCalculatedValueProperty, value);
}
}
#endregion
}
public class notifybase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
protected virtual void notifyPropertyChanged(string propertyName)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);
}
}
static class extensions
{
public static void AddHandler(this DependencyProperty prop, object component, EventHandler handler)
{
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(prop, component.GetType());
if (dpd != null)
dpd.AddValueChanged(component, handler);
}
}
}
The reason why it works when you uncomment //setdata(); is because it is initializing the object in what is effectively your viewmodel, therefore you can change its properties via binding. To clarify as a side note, data would be your view model, and dataitem is your model, however you're dataitem is using INPC, so it doesn't really make sense in this case to have a viewmodel necessarily.
Anyways, the issue is that TextBoxCalculatedValue is set to a OneWayToSource binding. When you run the code commented out, its going to try and bind to a null value. When it does, it tries to update a null value, which isn't possible. WPF handles what would normally be a null exception automatically. When you update the dataItem by clicking the button, it doesn't update the object TextBoxCalc is bound to, so instead, it will continue trying to bind & update the null object. Change it to a TwoWay binding and you'll see a difference. Changing to TwoWay is probably your best option.
Good practice is to use constructor injection to practice dependency injection. With that being said, passing a dataItem to data would be the best route, and at the very least, initializing dataItem in data's constructor would be an ideal approach. So,
public data(dataItem item)
{
Item = item;
}
or
public data()
{
Item = new dataitem();
}
for my project, i am trying to create a signal channel generator which connects to a toolset and pushes signals into it.
the issue i have is that i have been given the project in a form where the code for the textboxes are in the codebehind file, and i would like them to be in the xaml.
i have a variable which controls the number of channels (viewmodels) which can be changed. which is able to create multiple instances of the same viewmodel on the window. this allows the ability to select different targets inside the tool whcih it is communicating with and be able to pump signals to each target.
here is the code currently in the XAML:
<Window x:Class="SigGeneratorMVVM.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SigGeneratorMVVM"
Title="Signal Generator" Height="370" Width="734" >
<StackPanel Name="MyWindow">
<!--<TextBox Height="23" HorizontalAlignment="Left" Margin="91,20,0,0" Name="CurrentValDisplay" VerticalAlignment="Top" Width="120" />-->
</StackPanel>
</Window>
here is the code for the mainwindow.cs
public partial class MainWindow : Window
{
List<ViewModel> gViewModels;
int gNumChannels = 1;
private System.Threading.Timer mViewUpdateTimer;
private TimerCallback mViewTimerCallback;
private UtilityParticipant mParticipant;
public MainWindow()
{
InitializeComponent();
// Connect as UtilityParticipant
ConnectMesh();
gViewModels = new List<ViewModel>();
for (int i = 0; i < gNumChannels; i++)
{
gViewModels.Add(new ViewModel(mParticipant));
TextBlock CurrentValueText = new TextBlock();
CurrentValueText.Text = "Current Value:";
CurrentValueText.Margin = new Thickness(5);
TextBox CurrentValueBox = new TextBox();
CurrentValueBox.Width = 120;
CurrentValueBox.Name = "CurrentValDisplay" + i.ToString();
CurrentValueBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentValueBox.Margin = new Thickness(10);
CurrentValueBox.SetBinding(TextBox.TextProperty, "CurrentValue");
//CurrentValDisplay.Name = "CurrentValDisplay" + i.ToString();
//CurrentValDisplay.SetBinding(TextBox.TextProperty, "CurrentValue");
TextBlock CurrentFrequencyText = new TextBlock();
CurrentFrequencyText.Text = "Frequency:";
CurrentFrequencyText.Margin = new Thickness(5);
TextBox CurrentFrequencyBox = new TextBox();
CurrentFrequencyBox.Width = 120;
CurrentFrequencyBox.Name = "CurrentFrequencyDisplay" + i.ToString();
CurrentFrequencyBox.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
CurrentFrequencyBox.Margin = new Thickness(10);
CurrentFrequencyBox.SetBinding(TextBox.TextProperty, "Frequency");
Slider FrequencySlider = new Slider();
FrequencySlider.Width = 200;
FrequencySlider.Name = "FrequencySet" + i.ToString();
FrequencySlider.Value= 10;
FrequencySlider.Maximum = 10;
FrequencySlider.Minimum = 0.1;
FrequencySlider.SetBinding(Slider.ValueProperty, "Frequency");
//Create a new stackpanel
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Vertical;
//Set DataContext of the StackPanel
sp.DataContext = gViewModels[i];
//Add controls created above to the StackPanel
sp.Children.Add(CurrentValueText);
sp.Children.Add(CurrentValueBox);
sp.Children.Add(CurrentFrequencyText);
sp.Children.Add(CurrentFrequencyBox);
sp.Children.Add(FrequencySlider);
//Add the StackPanel to the window
MyWindow.Children.Add(sp);
}
mViewTimerCallback = this.UpdateView;
mViewUpdateTimer = new System.Threading.Timer(mViewTimerCallback, null, 100, 20);
}
Update: I already have a ViewModel which has get set methods for each property (CurrentValue and Frequency for now), would it be sufficient to bind the DataTemplate and ItemsControl to that instead of creating a new model class?
private SigGenChannel mSigGenChannel;
//Constructor
public ViewModel(UtilityParticipant aParticipant)
{
mSigGenChannel = new SigGenChannel(aParticipant);
}
public string CurrentValue
{
get
{
return mSigGenChannel.CurrentValue.ToString();
}
set
{
mSigGenChannel.CurrentValue = double.Parse(value);
RaisePropertyChanged("CurrentValue");
}
}
public double Frequency
{
get
{
return mSigGenChannel.Frequency;
}
set
{
mSigGenChannel.Frequency = value;
RaisePropertyChanged("Frequency");
}
}
public double Amplitude
{
get
{
return mSigGenChannel.Amplitude;
}
set
{
mSigGenChannel.Amplitude = value;
RaisePropertyChanged("Amplitude");
}
}
public void RefreshValue()
{
//A bit of a cheat, but we provide a means to poke the Viewmodel
//And raise a property change event
RaisePropertyChanged("CurrentValue");
}
also this is the SigChannel model:
class SigGenChannel
{
#region Private members
private UtilityParticipant mParticipant;
private double mCurrentValue;
private double mFrequency;
private double mAmplitude;
private double mTarget;
private double mOffset;
private double mCurrentStepTime;
private DateTime mStartTime;
private System.Threading.Timer mTimer;
private TimerCallback mTCallback;
private int mUpdateInterval = 10;
#endregion
#region Public members
public double CurrentValue
{
get
{
return mCurrentValue;
}
set
{
mCurrentValue = value;
}
}
public double Frequency
{
get
{
return mFrequency;
}
set
{
mFrequency = value;
}
}
public double Amplitude
{
get
{
return mAmplitude;
}
set
{
mAmplitude = value;
}
}
public double Target
{
get
{
return mTarget;
}
set
{
mTarget = value;
}
}
#endregion
//Constructor
public SigGenChannel(UtilityParticipant aParticipant)
{
mParticipant = aParticipant;
mCurrentValue = 10;
mFrequency = 200;
mAmplitude = 100;
mOffset = 0;
mCurrentStepTime = 0;
mStartTime = DateTime.Now;
mTCallback = this.Update;
mTimer = new System.Threading.Timer(mTCallback, null, 500, mUpdateInterval);
//Array enumData = Enum.GetNames;
//RefreshItems();
//Temp Code....!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
lCollection.Publish();
}
private void Update(object StateInfo)
{
TimeSpan span = DateTime.Now - mStartTime;
mCurrentStepTime = span.TotalMilliseconds / (double)1000;
mCurrentValue = (Math.Sin(mCurrentStepTime * (mFrequency * 2 * Math.PI)) * mAmplitude / 2) + mOffset;
//Temp Code...!
Collection lCollection = mParticipant.GetCollection("DefaultNodeName.NodeStats");
Parameter lParameter = lCollection.GetParameter("CPUPercent");
lParameter.SetValue(mCurrentValue);
lCollection.Send();
The current code is written in a way that does not follow the WPF suggested practices, and swimming against the current makes things a lot harder than then should be.
What the code should be doing is:
Create a (view)model class for a channel
For example:
class ChannelModel
{
public int Value { get; set; }
public int Frequency { get; set; }
}
Use an ItemsControl instead of a StackPanel
The WPF way of doing things like this is to bind controls to collections, so replace the StackPanel with an ItemsControl.
Bind the ItemsControl to an ObservableCollection of the models
Your main viewmodel should expose an ObservableCollection<ChannelModel> property and the control should bind to that directly:
<ItemsControl ItemsSource="{Binding CollectionOfChannelModels}"/>
This ensures that the control is automatically updated with any changes made to your collection without your needing to do anything else.
Use a DataTemplate to specify how each model should render
So far we 've gotten the control to stay in sync with your channel collection, but we also need to tell it how each item (channel model) should be displayed. To do this, add a DataTemplate to the Resources collection of the ItemsControl:
<ItemsControl.Resources>
<DataTemplate DataType={x:Type local:ChannelModel}>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Value" />
<TextBox Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
Generally the idea is to create a DataTemplate for a specific type, in your case it is for ViewModel.
Create a DataTemplate for your ViewModel, for instance:
<DataTemplate DataType={x:Type local:ViewModel}>
<TextBox Text="{Binding ViewModelTextProperty}" />
</DataTemplate>
And also in your XAML you must bind your list of ViewModels
<ItemsControl ItemsSource="{Binding myListOfViewModels}"/>
I created a custom password box user control which is able to show and hide the password. It just swaps out the standard password box with a textbox which is bound to the same password string property. It all works fine but now my data validation errors are no more shown, although they are being generated correctly in the background. Here's the xaml from my user control:
<UserControl x:Class="Controls.EAPPasswordBox"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400" x:Name="_root">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top">
<PasswordBox x:Name="pwdBox" Password="{Binding Password, Mode=TwoWay,ValidatesOnDataErrors=True}" />
<TextBox x:Name="txtBox" Text="{Binding Password, Mode=TwoWay,ValidatesOnDataErrors=True}" />
</StackPanel>
</Grid>
Here's how I use it in a view:
<local:EAPPasswordBox x:Name="pwdBox"
Grid.Column="1" Grid.Row="0" Grid.ColumnSpan="2" Password="{Binding password,Mode=TwoWay, ValidatesOnDataErrors=True}" ShowText="{Binding showPassword,Mode=TwoWay}"></local:EAPPasswordBox>
in the Parent view's viewmodel we implemented IDataErrorInfo like this:
public string this[string columnName]
{
get
{
string Result = "";
switch(columnName.ToLower())
{
case "password":
{
Result = Validatepassword();
break;
}
case "password2":
{
Result = Validatepassword2();
break;
}
default:
{
Result = this.ValidateStringValue(columnName);
break;
}
}
return Result;
}
}
Now when I enter text in the custom password box, the validation logic is called just fine but it's not displayed. Do I have to adjust my user control for this?
EDIT: Here's the code behind of my passwordbox:
public partial class EAPPasswordBox : UserControl, INotifyPropertyChanged
{
public bool ShowText
{
get { return (bool)GetValue(ShowTextProperty); }
set {
SetValue(ShowTextProperty, value);
if (value == true)
{
this.pwdBox.Visibility = System.Windows.Visibility.Collapsed;
this.txtBox.Visibility = System.Windows.Visibility.Visible;
}
else
{
this.pwdBox.Visibility = System.Windows.Visibility.Visible;
this.txtBox.Visibility = System.Windows.Visibility.Collapsed;
}
}
}
public string Password
{
get { return (string)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
private Visibility _PwdBoxVisibility;
public Visibility PwdBoxVisibility
{
get { return _PwdBoxVisibility; }
set
{
_PwdBoxVisibility = value; NotifyPropertyChanged("PwdBoxVisibility");
}
}
private Visibility _TxtBoxVisibility;
public Visibility TxtBoxVisibility
{
get { return _TxtBoxVisibility; }
set
{
_TxtBoxVisibility = value; NotifyPropertyChanged("TxtBoxVisibility");
}
}
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(string), typeof(EAPPasswordBox), null);
public static readonly DependencyProperty ShowTextProperty =
DependencyProperty.Register("ShowText", typeof(bool), typeof(EAPPasswordBox), new PropertyMetadata(OnShowTextPropertyChanged));
public EAPPasswordBox()
{
InitializeComponent();
this.pwdBox.SetBinding(PasswordBox.PasswordProperty, new System.Windows.Data.Binding() { Source = this, Path = new PropertyPath("Password"), Mode = BindingMode.TwoWay,ValidatesOnDataErrors=true });
this.txtBox.SetBinding(TextBox.TextProperty, new System.Windows.Data.Binding() { Source = this, Path = new PropertyPath("Password"), Mode = BindingMode.TwoWay, ValidatesOnDataErrors=true });
this.ShowText = false;
}
private static void OnShowTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EAPPasswordBox passwordBox = d as EAPPasswordBox;
if (passwordBox != null)
{
passwordBox.ShowText=(bool)e.NewValue;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
2nd Edit: It would also help if someone would just explain to me the basics of binding properties of usercontrols in the xaml of a parent window/control. I dont quite understand why the usercontrol doesnt get the property changed events of the corresponding parent views viewmodel properties since it is bound to those via xaml.
Here's my solution at last. Since I realized that the DataContext of the user control automatically is the ViewModel of the parent view, I dumped the binding of the Password dependency property completely. I introduced a new parameter in the control which has to be set to the password property of the parent view model. I then use this string to do a manual binding of the textbox and the password box in the loaded event of the control. Here's my code:
public partial class EAPPasswordBox : UserControl, INotifyPropertyChanged
{
public bool ShowText
{
get { return (bool)GetValue(ShowTextProperty); }
set {
SetValue(ShowTextProperty, value);
if (value == true)
{
this.pwdBox.Visibility = System.Windows.Visibility.Collapsed;
this.txtBox.Visibility = System.Windows.Visibility.Visible;
}
else
{
this.pwdBox.Visibility = System.Windows.Visibility.Visible;
this.txtBox.Visibility = System.Windows.Visibility.Collapsed;
}
}
}
public string PasswordPropertyName { get; set; }
private Visibility _PwdBoxVisibility;
public Visibility PwdBoxVisibility
{
get { return _PwdBoxVisibility; }
set
{
_PwdBoxVisibility = value; NotifyPropertyChanged("PwdBoxVisibility");
}
}
private Visibility _TxtBoxVisibility;
public Visibility TxtBoxVisibility
{
get { return _TxtBoxVisibility; }
set
{
_TxtBoxVisibility = value; NotifyPropertyChanged("TxtBoxVisibility");
}
}
public static readonly DependencyProperty ShowTextProperty =
DependencyProperty.Register("ShowText", typeof(bool), typeof(EAPPasswordBox), new PropertyMetadata(OnShowTextPropertyChanged));
public EAPPasswordBox()
{
InitializeComponent();
this.ShowText = false;
}
private static void OnShowTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EAPPasswordBox passwordBox = d as EAPPasswordBox;
if (passwordBox != null)
{
passwordBox.ShowText=(bool)e.NewValue;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private void _root_Loaded(object sender, RoutedEventArgs e)
{
this.pwdBox.SetBinding(PasswordBox.PasswordProperty, new System.Windows.Data.Binding() { Source = this.DataContext, Path = new PropertyPath(PasswordPropertyName), Mode = BindingMode.TwoWay, ValidatesOnDataErrors = true });
this.txtBox.SetBinding(TextBox.TextProperty, new System.Windows.Data.Binding() { Source = this.DataContext, Path = new PropertyPath(PasswordPropertyName), Mode = BindingMode.TwoWay, ValidatesOnDataErrors = true });
}
}
Here's the XAML of the control.
<UserControl x:Class="GAB.EAP2011.Controls.EAPPasswordBox"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400" x:Name="_root" Loaded="_root_Loaded">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Top">
<PasswordBox x:Name="pwdBox" />
<TextBox x:Name="txtBox" />
</StackPanel>
</Grid>
Here's how to use it:
<local:EAPPasswordBox x:Name="pwdBox"
Grid.Column="1" Grid.Row="0" Grid.ColumnSpan="2" PasswordPropertyName="password" ShowText="{Binding showPassword,Mode=TwoWay}"></local:EAPPasswordBox>
Now you got a nice password visibility switcher control :)
Comments appreciated!