I have some checkboxes I'm trying to bind to each other in somewhat a group fashion and a singular fashion. For example Checkbox A binds to Checkboxes C,D and E as well as a boolean value in my project.properties.default if A is checked or unchecked.(a group fashion) Checkbox C binds to a project.properties.default value if checked. Checkbox D binds to a project.properties.default value if checked and so on....(singular fashion). And I'm not sure I'm going about it the right way. I'm trying to use MultiBinding in WPF. MultiBinding says I need to use a convertor? I'm not sure why I need to convert anything, but since WPF is calling for it I have made a class that inherits from IMultiValueConverter. I can get my program to work partially if I put in the following code:
public class EmulationBindingCheckBox :IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
foreach (object value in values)
{
if ((value is bool) && (bool)value == true)
{
return true;
}
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return(targetTypes.ToArray());
}
}
If I press on my group check box A it fires the Convert method and the single ones are selected and saved when I close my window. But I'm not sure what to put in the convert back method, because that's what seems to be triggering the event /convertback method if the single boxes are checked. When I click on the singular checkbox the isChecked won't bind and save if I close my window. It fires the ConvertBack method and I've returned targettypes.toArray(), because that's the only thing that has stopped a red square box around my checkbox when checked. If I use return null it doesn't like it. I'm not understanding why I need to convert anything, so I'm not sure on how to convert back anything. I just know that the convert method works for multi select. Does anyone know why this convert thing is needed in multiBinding? Or is there a better way to do this?
Your post is describing come complex logic interactions, and shows an attempt to manage this using an IMultiValueConverter
, It goes on to describe some behaviors that could perhaps be attributed to circularity in the logic, which is sometimes tricky to avoid with bound properties. And finally, you ask:
is there a better way to do this?
I can only say that there is at least one alternative way to go about it: Instead of overburdening an IMultiValueConverter
you could try moving all of the logic into the view model/data context where you could mitigate circularity and/or conflicting logic with a boolean, semaphore, or reference-counting scheme that is owned by the "first property to change" and that persists until all the changes have propagated. In short that is my suggestion, and the rest of this answer is just details to hopefully make it clearer what I mean by that.
Simple Case using One-Hot Checkboxes
Here's the basic idea: suppose that any checkbox ☐ 1, ☐ 2, ☐ 3, or ☐ 4 will cancel the other three if it becomes checked. This is trivial to implement in the VM if checkboxes follow this representative boolean binding:
class MainWindowDataContext : INotifyPropertyChanged
{
public bool One
{
get => _one;
set
{
if (!Equals(_one, value))
{
_one = value;
if(One)
{
Two = false;
Three = false;
Four = false;
}
OnPropertyChanged();
}
}
}
bool _one = default;
.
.
.
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Minimal Example of Deliberately Conflicting Logic
But if four checkboxes for ☐ All, ☐ Odd, ☐ Even and ☐ None are added, it creates an immediate and obvious conflict. These, too, are one-hot with respect to each other but it's also plain to see that if setting the All checkbox attempts to check all the singles in turn, when every single is wired to cancel all of the other singles when toggled true, then we've got a real problem.
This property implementation for the All checkbox will not work. It conflicts with the one-hot behavior of the single checkboxes.
public bool All
{
get => _all;
set
{
if (!Equals(_all, value))
{
_all = value;
if (All)
{
Odd = false;
Even = false;
None = false;
One = true;
Two = true;
Three = true;
Four = true;
}
OnPropertyChanged();
}
}
}
bool _all = default;
Solution using IDisposable Ref Counting
The logic scheme can be made stateful very easily, where the first property to change checks out an IDisposable
token that suppresses the property change logic and events of all the other properties that occur within a using block. Then, when the token disposes, the UI is updated en masse by firing all of the property changes to push the backing store values to the UI. You can use "any" ref counting scheme or maybe even get away with using a simple bool flag. I chose this particular NuGet only because I'm so familiar with it. Here, a singleton instance of DisposableHost
is set up to fire the notifications when the token count goes to zero, and two representative properties show how the using
blocks work.
// <PackageReference Include="IVSoftware.Portable.Disposable" Version="1.2.0" />
using IVSoftware.Portable.Disposable;
class MainWindowDataContext : INotifyPropertyChanged
{
public bool One
{
get => _one;
set
{
if (!Equals(_one, value))
{
_one = value;
if (RefCount.IsZero())
{
using (RefCount.GetToken())
{
if (One)
{
Two = false;
Three = false;
Four = false;
All = false;
Odd = false;
Even = false;
None = false;
}
OnPropertyChanged();
}
}
}
}
}
bool _one = default;
.
.
.
public bool All
{
get => _all;
set
{
if (!Equals(_all, value))
{
_all = value;
if (RefCount.IsZero())
{
using (RefCount.GetToken())
{
if (All)
{
Odd = false;
Even = false;
None = false;
One = true;
Two = true;
Three = true;
Four = true;
}
OnPropertyChanged();
}
}
}
}
}
bool _all = default;
.
.
.
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Debug.WriteLine($"Originator: {propertyName}" );
}
public event PropertyChangedEventHandler? PropertyChanged;
// <PackageReference Include="IVSoftware.Portable.Disposable" Version="1.2.0" />
public DisposableHost RefCount
{
get
{
if (_refCount is null)
{
_refCount = new DisposableHost();
_refCount.FinalDispose += (sender, e) =>
{
foreach (var propertyName in new[]
{
nameof(One), nameof(Two), nameof(Three), nameof(Four),
nameof(All), nameof(Even), nameof(Odd), nameof(None),
})
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
};
}
return _refCount;
}
}
DisposableHost? _refCount = default;
}
(From Comment) Integrating with Properties.Settings.Default
If you have a settings resource, then it should be used as the canonical backing store. The properties in this case would follow this pattern:
public bool One
{
get => Properties.Settings.Default.One;
set
{
if (!Equals(Properties.Settings.Default.One, value))
{
Properties.Settings.Default.One = value;
if (RefCount.IsZero())
{
using (RefCount.GetToken())
{
if (One)
{
Two = false;
Three = false;
Four = false;
}
All = false;
Odd = false;
Even = false;
None = false;
OnPropertyChanged();
}
}
}
}
}
.
.
.
public DisposableHost RefCount
{
get
{
if (_refCount is null)
{
_refCount = new DisposableHost();
_refCount.FinalDispose += (sender, e) =>
{
foreach (var propertyName in new[]
{
nameof(One), nameof(Two), nameof(Three), nameof(Four),
nameof(All), nameof(Even), nameof(Odd), nameof(None),
})
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// This ensures a single save operation
// after all the properties have settled.
Properties.Settings.Default.Save();
};
}
return _refCount;
}
}
DisposableHost? _refCount = default;