I have extended the TreeViewItem class to allow me to store extra data within a tree view item. I would like to be able to set the style of the treeview item based on the value of one of the extended properties I have added.
So far I have:
namespace GX3GUIControls
{
public class GX3TreeViewItem : TreeViewItem
{
public bool Archived { get; set; }
public object Value { get; set; }
}
}
<src:GX3ClientPlugin.Resources>
<Style TargetType="{x:Type Controls:GX3TreeViewItem}">
<Style.Triggers>
<DataTrigger Archived="True">
<Setter Property="Background" Value="Gray" />
<Setter Property="FontStyle" Value="Italic" />
</DataTrigger>
</Style.Triggers>
</Style>
</src:GX3ClientPlugin.Resources>
But I get the error - Error 1 The property 'Archived' was not found in type 'DataTrigger
DataTrigger has no Archived property, but you can bind your Achived-property to it via the Binding property like so <DataTrigger Binding="{Binding Path=Archived}" Value="True">
To notify your view if the Achived property changes, you could either:
1.Implement the INotifyPropertyChanged Interface in your GX3TreeViewItem-class: public class GX3TreeViewItem : TreeViewItem, INotifyPropertyChanged, create a method which raises the PropertyChanged Event:
private void PropertyChanged(string prop)
{
if( PropertyChanged != null )
{
PropertyChanged(this, new PropertyChangedEventArgs(prop);
}
}
and place this method in the setter of your property:
private bool _achived;
public bool Achived
{
get
{
return _achived;
}
set
{
_achived = value;
PropertyChanged("Achived");
}
}
2.Or make your property a DependencyProperty.
Honestly it seems like you're doing it wrong. Those properties should be on your data.
You can do something like this,
Style="{Binding Path=Archived, Converter={StaticResource GetStyle}}"
GetStyle is an IValueConverter, no need to extend TreeView imo.
This is not the correct way to implement this. you should take a look at the MVVM Pattern.
Your UI is not the proper place to "store extra data". UI is UI and data is data. This is the worst mistake done by people coming from a winforms or otherwise non-WPF background, using a wrong approach and a wrong mindset in WPF.
This will either not work (because the ItemContainerGenerator of the TreeView knows nothing about your class, or require extra work in overriding the default behavior of such class.
Related
This question already has answers here:
Preventing Memory Leaks with Attached Behaviours
(11 answers)
Prevent memory leaks in WPF
(4 answers)
How to deinitialize a FrameworkElement?
(3 answers)
What is the "Weak Event" pattern used in WPF applications?
(3 answers)
Closed 5 years ago.
I'm using MVVM to create an application which switches between multiple views. The views are instantiated through a ContentControl like this:
<ContentControl Name="DynamicViewControl" Content="{Binding }">
<ContentControl.Resources>
<DataTemplate x:Key="PageOneTemplate">
<pages:PageOne DataContext="{Binding PageOneViewModel}"/>
</DataTemplate>
<DataTemplate x:Key="PageTwoTemplate">
<pages:PageTwo DataContext="{Binding PageTwoViewModel}"/>
</DataTemplate>
<!-- And so on... -->
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentPage}" Value="{x:Static model:Pages.PageOne}">
<Setter Property="ContentTemplate" Value="{StaticResource PageOneTemplate}"/>
</DataTrigger>
<DataTrigger Binding="{Binding CurrentPage}" Value="{x:Static model:Pages.PageTwo}">
<Setter Property="ContentTemplate" Value="{StaticResource PageTwoTemplate}"/>
</DataTrigger>
<!-- And so on... -->
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
Where the underlying ViewModel looks something like this:
public enum Pages {
PageOne,
PageTwo,
// etc...
}
public class PageViewModel : ObservableObject {
private Pages currentPage = Pages.PageOne;
public PageViewModel() {
PageOneViewModel= new PageOneViewModel(Model);
PageTwoViewModel= new PageTwoViewModel(Model);
// And so on...
NavButtonCommand = new RelayCommand(NavButton);
PreviousButtonCommand = new RelayCommand(PreviousButton);
}
public PageModel Model { get; } = new PageModel();
/// <summary>Gets or sets the current page.</summary>
public Pages CurrentPage {
get => currentPage;
set {
currentPage = value;
NotifyPropertyChanged();
}
}
public DataSelectionViewModel PageOneViewModel { get; }
public ProfileSelectionViewModel PageTwoViewModel { get; }
public ICommand NavButtonCommand { get; }
public ICommand PreviousButtonCommand { get; }
// This isn't my actual page change logic, just some code with a
// similar effect to get my point across
private void NavButton(object param) {
int next = (int)CurrentPage + 1;
if Enum.IsDefined(typeof(Pages), next) {
CurrentPage = (Pages)next;
}
}
private void PreviousButton(object param) {
int previous = (int)CurrentPage - 1;
if Enum.IsDefined(typeof(Pages), previous) {
CurrentPage = (Pages)previous;
}
}
}
The problem is that in some of my Views, I need to subscribe to PropertyChanged notifications on their respective ViewModels so that I can change things in my View that can't be bound easily. This results in a "memory leak" (insofar as a memory leak can exist in C#) because the ContentControl creates a new View every time and it never gets cleaned up due to those event handlers still having references in the ViewModel.
I tried cleaning up all event subscribers to the ViewModel when the View changes, but quite apart from the fact that it results in view cleanup code inside the ViewModel, it also had unintended consequences, and made some of my functionality stop working.
Is there a way for me to tell my views to stop subscribing to events? Or, should I find a way to bind them (such as creating a custom control with DependencyProperties which can be bound).
I found the answer, quicker than I thought. The way that most WPF controls do it turns out to be the Weak Event Pattern. This pattern allows you to subscribe to events with a weak reference. The solution was to change lines like this:
model.PropertyChanged += Model_PropertyChanged;
To something more like this:
PropertyChangedEventManager.AddHandler(model, Model_PropertyChanged, "MyProperty");
That way, even if the ViewModel has a longer lifetime than the view, any references will only be weak ones, allowing the garbage collector to come along and clean up the object even though its event subscriptions haven't been cleaned up.
I have dialogbox with Content control with templates:
<ContentControl Content="{Binding Model,UpdateSourceTrigger=PropertyChanged}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
and property change event at dialogbox context:
dialogContext.Model.PropertyChanged += (s, e) => Change(s,e, context);
private void Change(object s, PrropertyChangeEventArgs e, Context context)
{
...
context.Mode = new Example()
{
...
}
model.PropertyChanged += (sender, eventArgs) =>
ModelChange(sender, eventArgs, context);
context.Model = model;
}
I want to change some properties at model, that determine which custom template will be displayed.
To reload new template and invoke temlate selector should I create new model and
add property change event to this. Is is ok, or is it another way to do this.
Update
The below implementation doesn't work because it turns out that the template selector is only reinvoked if the actual value of ContentControl.Content changes. If you've still got the same instance of Model, raising PropertyChanged will have no effect. I even tried overriding ModelClass.Equals() and ModelClass.GetHashCode(). Neither was called. Maybe the Binding is calling Object.ReferenceEquals().
But I did find three ways to do this. All have been tested, now that I've learned my lesson.
If you're going to this much trouble to get a template selector to work, best to look for some other approach where you're not fighting the framework.
You could instead use style triggers to swap templates:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Model.Foo}" Value="foo">
<Setter
Property="ContentTemplate"
Value="{StaticResource Foo}"
/>
</DataTrigger>
<DataTrigger Binding="{Binding Model.Foo}" Value="bar">
<Setter
Property="ContentTemplate"
Value="{StaticResource Bar}"
/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
...but the logic in your template selector may be quite a bit more complicated than that, in which case it may not be feasible.
Here's another. You don't need a template selector to select a template. A converter can return a DataTemplate too, and if you use a multi-binding converter, you can give it whatever it needs to look up a DataTemplate in the resources:
<ContentControl
Content="{Binding Model}"
>
<ContentControl.ContentTemplate>
<MultiBinding
Converter="{StaticResource ContentTemplateConverter}"
>
<!--
We must bind to Model.Foo so the binding updates when that changes,
but we could also bind to Model as well if the converter wants to
look at other properties besides Foo.
-->
<Binding Path="Model.Foo" />
<!-- The ContentControl itself will be used for FindResource() -->
<Binding RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</ContentControl.ContentTemplate>
</ContentControl>
C#
public class ContentTemplateConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var ctl = values[1] as FrameworkElement;
switch ($"{values[0]}")
{
case "foo":
return ctl.FindResource("Foo") as DataTemplate;
case "bar":
return ctl.FindResource("Bar") as DataTemplate;
}
return null;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
One last possibility, and in my opinion the least, is to use the template selector, but make it work by actually replacing the value of Model every time one of its properties changes. Rewrite ModelClass so it can easily be cloned:
public ModelClass() {}
public ModelClass(ModelClass cloneMe) {
this.Foo = cloneMe.Foo;
this.Bar = cloneMe.Bar;
}
...and keep _model_PropertyChanged from my original answer, but change the guts so instead of merely raising PropertyChanged, it replaces the actual value of Model (which will of course still raise PropertyChanged, as a side effect):
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ModelClass.Foo))
{
Model = new ModelClass(Model);
}
}
I've tested that and while it's alarmingly goofy, it does work.
Instead of cloning ModelClass, you could use a "reference" class for the parent's Model property:
public class ModelClassRef {
public ModelClassRef(ModelClass mc) { ... }
public ModelClassRef { get; private set; }
}
But it's still wicked goofy. The viewmodel shouldn't "know" the view even exists, but here you are rewriting a chunk of it in a bizarre way just to work around a peculiarity in the implementation of a particular control. View workarounds belong in the view.
So when this.Model.Foo changes, you want to change the template? I would expect this to do the job:
#region Model Property
private ModelClass _model = null;
public ModelClass Model
{
get { return _model; }
set
{
if (value != _model)
{
if (_model != null)
{
_model.PropertyChanged -= _model_PropertyChanged;
}
_model = value;
if (_model != null)
{
_model.PropertyChanged += _model_PropertyChanged;
}
OnPropertyChanged();
}
}
}
private void _model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// If Model.Foo changed, announce that Model changed. Any binding using
// the Model property as its source will update, and that will cause
// the template selector to be re-invoked.
if (e.PropertyName == nameof(ModelClass.Foo))
{
OnPropertyChanged(nameof(Model));
}
}
This is defined in your viewmodel base class. Maybe you've already got essentially the same method and it's called something else; if so, use that one of course.
protected void OnPropertyChanged([CallerMemberName] String propName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
By the way, get rid of UpdateSourceTrigger=PropertyChanged. ContentControl will never create a new value for its Content property and pass that back to your viewmodel through the binding. Can't, won't, and you wouldn't want it to. So you don't need to tell it exactly when to perform a task it's not capable of performing.
I'm having this code where I would like to update the image in my main window when the property in a usercontrol changes. But somehow I can't get the trigger working.
Some of the XAML code
<StatusBar MinHeight="10" MaxHeight="20" VerticalAlignment="Bottom" Grid.Row="2">
<Image x:Name="SomeNameHere">
<Image.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=pingable}" Value="false">
<Setter Property="Image.Source" Value="Icons/MainWindow/StatusOffline_stop_32x.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=pingable}" Value="true">
<Setter Property="Image.Source" Value="Icons/MainWindow/StatusOK_32x.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</StatusBar>
The part where the property comes from
public bool pingable { get; set; }
public MainWindow()
{
InitializeComponent();
pingable = PingHost("some random IP");
}
public bool PingHost(string nameOrAddress)
{
pingable = false;
Ping pinger = new Ping();
try
{
PingReply reply = pinger.Send(nameOrAddress);
pingable = reply.Status == IPStatus.Success;
}
catch (PingException)
{
// Discard PingExceptions and return false;
}
return pingable;
}
I see the property during debugging in the XAML editor so it seemingly gets recognized and I also see the value would fit. But somehow the setter doesn't get executed.
Someone an idea on this?
Thanks and have a nice day!
You need to raise your PropertyChanged event on pingable to get the view to update.
Basically, in order to get the view to know that it needs to update some control based on a binding, your view model needs to implement INotifyPropertyChanged; any time you want the update the view based on a change in the view model, you need to raise PropertyChanged from the view model and pass it the name of the bound property whose value was updated.
If you RaisePropertyChanged on the getter and setter, it will notify the XAML when the property changes and make the triggers fire.
Try this code:
bool _pingable;
public bool pingable
{
get{return _pingable;}
set{ _pingable = value; RaisePropertyChanged;}
};
While the other two answers before mine point out that INotifyPropertyChanged needs to be implemented -- and that is somewhat important for a normal WPF application -- I don't believe that actually matters here. Your ping request looks synchronous, so the UI isn't actually going to be loaded by the time you update your value, making INotifyPropertyChanged irrelevant.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Thread.Sleep(2000); // pretending to ping
Flag = true;
}
public bool Flag { get; set; }
}
XAML:
<Grid>
<CheckBox IsChecked="{Binding Flag}" />
</Grid>
This is checked when the window eventually loads. If this were asynchronous, it would be a different story.
I see the property during debugging in the XAML editor
I'm not sure what this means. I suspect the actual problem here is that you don't have a DataContext defined. (Maybe it is, but I can't see it here!)
You've created the property on the window itself, so... what if you switched your trigger to be...
<DataTrigger Binding="{Binding Path=pingable, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}" Value="false">
I need to bind the IsEnabled property of a set of ListBoxItems to a bool property value that resides in the DataContext. I have followed several tutorials to get to where I am however I am still not having any luck. In my XAML I have defined a setter within a ListBox.ItemContainerStyle as follows:
<ListBox Name="Requests">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsEnabled"
Value="{Binding IsEnabled}"/>
The bool property value resides in a class set as the DataContext as such:
public class dcSystemRequests : INotifyPropertyChanged
{
private bool _IsEnabled;
public bool IsEnabled
{
get
{
return _IsEnabled;
}
set
{
if (_IsEnabled != value)
{
_IsEnabled = value;
NotifyPropertyChanged("IsEnabled");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyChanged)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyChanged));
}
}
Now when I modify the property I am not seeing the value reflected in the UI as expected; the property value is being changed in the code like this:
((dcSystemRequests)DataContext).IsEnabled = !((dcSystemRequests)DataContext).IsEnabled;
Since this is proprietary software I only included what I think is necessary to understand the issue but will happily provide more if needed. Any advice or guidance is greatly appreciated.
If the IsEnabled property is part of the ListBox's DataContext, then you need to use a RelativeSource binding:
<Style TargetType="ListBoxItem">
<Setter Property="IsEnabled"
Value="{Binding DataContext.IsEnabled, RelativeSource={RelativeSource AncestorType=ListBox}"/>
</Style>
The DataContext of the ListBoxItems is each corresponding Data Item.
See ItemsControl for more information.
Your ItemContainerStyle has not the same DataContext as your ListBox, but the data of the ListBox's items. Therefore binding to the IsEnabled property makes no sense if you do not set the binding Source to the ListBoxItem's parent.
I am attempting to implement some simple validation on a textbox in MVVM
public string Property
{
get
{
if (App.PropertyStorageContainer != null)
{
return App.PropertyStorageContainer.Property;
}
else
{
return null;
}
}
set
{
App.PropertyStorageContainer.Property = value;
RaisePropertyChanged("Property");
}
}
Then in my PropertyStorageContainer class I have
private string _property;
public string Property
{
get
{
return App.PropertyStorageContainer.Property;
}
set
{
if(value meets some condition)
{
_property = value;
}
else
{
_property = someothervalue;
}
}
}
.
<TextBox Width="50" TextAlignment="Center" Text="{Binding Property, Mode=TwoWay, NotifyOnValidationError=True}" MaxLength="3"></TextBox>
The point of this is to validate what goes in the box. Now if I set this value directly from my code then everything works as I would expect. It attempts to SET the value, then calls RaiseProperyChanged, then GET the value (which because of the validation may not be the same value that was entered originally). The final value retrieved does show up on the view, so I know TwoWay binding is working.
The problem I have is when the input for SET comes from the bound XAML property / directy from user. In this case the SET method is called, the validation performed, but the GET never happens. This results in the unvalidated value remaining in the textbox on screen.
My first question would be is this a bug or expected behavior? I can see how maybe they tried to save performance by removing that last GET when the input came straight from the user since there should be nothing new to GET. But if not then maybe the way I have it all setup is interfering with the GET being called.
Second question is of course any suggestions for getting around this one. I've read a few suggestions for other methods of doing validation, but my program is already live on PROD and most of the changes being suggested involve a lot of rework for me so I am hoping to find a way to make it call GET any time the property is SET.
I have made a couple of assumptions since I am not sure I understand you code completely but I think you could consider possibly implementing a custom validation rule. First off, since your custom ValidationRule will take care of the validation you could get the logic out of your model class's property definition and "dumb down" your poco:
class PropertyStorageContainer
{
public string Property { get; set; }
}
It seems you desire your view model to act as a basic wrapper around your model class. Again, I will assume this is valid based on the description of your scenario:
class PropertyStorageContainerViewModel : INotifyPropertyChanged
{
private PropertyStorageContainer model;
public PropertyStorageContainerViewModel(PropertyStorageContainer model)
{
this.model = model;
}
public string Property
{
get
{
if (model != null)
{
return model.Property;
}
else
{
return null;
}
}
set
{
if (model.Property != value)
{
model.Property = value;
RaisePropertyChanged("Property");
}
}
}
// INotifyPropertyChanged implementation...
}
Now create a new class that extends System.Windows.Controls.ValidationRule and override the abstract Validate method in order implement your validation logic. For the example, I created a rule that just checks if the string is null or empty (assuming that would be an invalid scenario):
class IsNullOrEmptyValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string s = (value ?? string.Empty).ToString();
if (string.IsNullOrEmpty(s))
{
// Invalid...
return new ValidationResult(false, "Please enter a value.");
}
else
{
// Valid...
return new ValidationResult(true, null);
}
}
}
Now for the XAML... Here is an example of a TextBox that adds the validation rule to its binding validation rules (can be multiple rules).
<TextBox Name="textBox1" Width="50" FontSize="12"
Validation.ErrorTemplate="{StaticResource validationTemplate}"
Style="{StaticResource textBoxInError}">
<TextBox.Text>
<Binding Path="Property" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<local:IsNullOrEmptyValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Then define the following resources (referenced above) somewhere (e.g., Window.Resources). First a ControlTemplate to define how the TextBox should look when in invalid state:
<ControlTemplate x:Key="validationTemplate">
<DockPanel>
<TextBlock Foreground="Red" FontSize="15" Text="!!!" />
<AdornedElementPlaceholder/>
</DockPanel>
</ControlTemplate>
Additionally you could define a style trigger to display the error message. Here I just bind it to the ToolTip property of the TextBox:
<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
You're going into INPC hell right now. I've been there and it's not fun.
That's a big no-no, especially since if any mapping is done on such classes, those getters and setters will be called outside of their WPF binding context and hell breaks lose.
Keep it simple: bind straight to App.PropertyStorageContainer.Property
For the second case, either:
Use data validation
Let the property be set not by binding but through a Command, in which you can do such value swap.
Do yourself a favor and don't abuse properties' get/set