C# + WPF : how should I do proper checks/guards/casts when accessing control values?
This is probably pretty basic, but I'm just picking up C# after many years working with other languages, and I've unfortunately grown used to loose and dynamic typing. Now, in building a WPF form with a lot of checkboxes, I notice that my code to do simple things like determine whether a checkbox is checked is actually quite complicated due to the need to check for nulls and cast the results. I end up with helper functions like:
private bool isChecked(CheckBox control) {
return control != null && control.IsChecked != null && control.IsChecked.HasValue && (bool)control.IsChecked;
}
so that in my logic code, I can just do
if (isChecked(opticsCheckBox))
{
// whatever I need to do if opticsCheckBox is checked
}
Is this the normal way of doing things in C# (with WPF), or am I missing something simple? Basically I'm finding the nested layers of conditionals to check every object for null all the time to be a warning sign of bad code (and the fact that I could forget a check). Not sure what I should be doing though.
Should I be using try ... catch everywhere even though the control not being present or checked isn't really an exceptional condition? That seems to me like it would end up being just as cluttered.
Another example to clarify: When I would like to write something like:
maxWeight = (int)maxWeightComboBox.SelectedItem;
I find myself instead writing:
if (maxWeightComboBox != null && maxWeightComboBox.SelectedItem != null)
{
ComboBoxItem item开发者_如何学运维 = (ComboBoxItem)maxWeightComboBox.SelectedItem;
maxWeight = Int32.Parse(item.Content.ToString());
}
WPF provides such features as notifications on property changes, dependency propeties, and binding. So the good practice in WPF is to use PresentationModel-View pattern or MVC pattern instead of direct access to controls.
Your presentation model (or contoller) have to handle all business logic, and view just reflect actual state of model.
In your case model looks like:
public class SampleModel : ObservableObject
{
private bool? _isFirstChecked;
public bool? IsFirstChecked
{
get
{
return this._isFirstChecked;
}
set
{
if (this._isFirstChecked != value)
{
this._isFirstChecked = value;
this.OnPropertyChanged("IsFirstChecked");
}
}
}
private int _maxWeight;
public int MaxWeight
{
get
{
return this._maxWeight;
}
set
{
if (this._maxWeight != value)
{
this._maxWeight = value;
this.OnPropertyChanged("MaxWeight");
}
}
}
public IEnumerable<int> ComboBoxItems
{
get
{
yield return 123;
yield return 567;
yield return 999;
yield return 567;
yield return 1999;
yield return 5767;
yield return 9990;
}
}
}
As we have to notify view with property changed event, we add Observable class, which implement this logic:
public class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var safePropertyChanged = this.PropertyChanged;
if (safePropertyChanged != null)
{
safePropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
So, now we have presentation model with declaration of necessary properties, let's see at view:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self ="clr-namespace:Test"
Title="MainWindow"
Height="350" Width="525">
<Window.Resources>
<self:NullableBoolToStringConvreter x:Key="nullableBoolToStringConverter" />
</Window.Resources>
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center">IsFirstChecked:</Label>
<CheckBox VerticalAlignment="Center"
IsChecked="{Binding Path=IsFirstChecked}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center">Max Weight:</Label>
<ComboBox ItemsSource="{Binding Path=ComboBoxItems}"
VerticalAlignment="Center"
SelectedValue="{Binding Path=MaxWeight}">
</ComboBox>
</StackPanel>
<TextBox Text="{Binding Path=MaxWeight}" />
<TextBox Text="{Binding Path=IsFirstChecked, Converter={StaticResource nullableBoolToStringConverter}}"/>
<Button Click="Button_Click" Content="Reset combo box to 999 and checkbox to null"/>
</StackPanel>
</Grid>
Also we have to modify this xaml code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var model = new SampleModel();
model.MaxWeight = 5767;
this.Model = model;
}
public SampleModel Model
{
get
{
return (SampleModel)this.DataContext;
}
set
{
this.DataContext = value;
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Model.MaxWeight = 999;
this.Model.IsFirstChecked = null;
}
}
As you can see we create SampleModel instance at MainWindow constructor, set up its properties and set model instance as DataContext of view.
After DataContext changed, WPF internal mechanizm starts binding process. For example, for combobox control it extracts model property ComboBoxItems and creates item containers. Then extracts property MaxValue and bind it to SelectedValue, i.e. combobox selection will point at value '5767'.
In demostration purposes I placed two text boxes, which display actual value of "MaxWeight" and "IsFirstChecked" properties. Default binding implementation shows empty strings on null values, so we have to add appropriate converter:
public class NullableBoolToStringConvreter : IValueConverter
{
private static readonly string _NullString = "Null";
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null ? NullableBoolToStringConvreter._NullString : value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can test application and ensures that changes of UI controls state automatically reflects in model. On the other hand, clicking on the button will reset model properties to defined values and UI immediatly reacts on it.
So with WPF you don't need access to controls. XAML and InitializeComponent() guarantee you that all controls are created.
As for checking:
control.IsChecked.HasValue && (bool)control.IsChecked
as was mentioned you can use expression
model.IsFirstChecked ?? false
or extension method:
public static class BooleanNullableExtensions
{
public static bool IsTrue(this Nullable<bool> value)
{
return value.HasValue && value.Value;
}
}
In general, yes, C# is a bit more verbose than a dynamic / lossely typed language. The same is true of Java. Looking at your specific examples ...
private bool isChecked(CheckBox control) {
return control != null && control.IsChecked != null && control.IsChecked.HasValue && (bool)control.IsChecked;
}
A couple of points ... the following two checks are equivalent:
control.IsChecked != null
control.IsChecked.HasValue
The IsChecked property is a Nullable type. As you are new to C# I would recommend reading up on value types vs. reference types. Once you have the hang of that, you can find out how the Nullable type can be used to wrap a value type in order to assign a null value to it. The page linked below explains why the twp statements above are equivalent:
http://msdn.microsoft.com/en-us/library/2cf62fcy%28VS.80%29.aspx
Secondly, why are you checking that control!=null ? In typical scenarious you create controls in XAML on your Window or UserControl, identifying via the x:Name attribute. In this case you can rely on the control being present in your UI and drop this check.
Your other two checks are necessary ;-)
Good idea placng these in a method that you can re-use. You can also 'extend' the langauge by creating extension methods, e.g
private bool IsChecked(this CheckBox control) {
return control.IsChecked.HasValue && (bool)control.IsChecked;
}
// calls the extension method above.
myCheckBox.IsChecked()
Hoep that helps.
There are a lot of ways to answer your question. I think the most important of these ways is to emphasize that in WPF, if you're writing code that explicitly manipulates UI controls, you're probably doing something wrong.
I can't emphasize this enough. To my mind the primary reason to use WPF at all is that it frees you from having to manipulate the UI in code.
Here's how my programs determine whether or not a checkbox is checked:
In XAML:
<CheckBox IsThreeState="false" IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
In the object bound to this view:
public bool IsChecked { get; set; }
The IsChecked
property of my object now always reflects the state of the checkbox in the UI. (The reverse is not true unless I implement change notification in my class.)
For your combo box example, I'd implement it like this. First, in XAML:
<ComboBox ItemsSource="{Binding Numbers}" SelectedItem="{Binding SelectedNumber, Mode=TwoWay}"/>
In the object bound to the view:
public IEnumerable<int> Numbers { get { return new[] { 1, 2, 3, 4, 5, 6 }; } }
public int? SelectedNumber { get; set; }
SelectedNumber
is nullable in this case so that you can test for the case where nothing was selected, e.g.:
Console.WriteLine(SelectedNumber == null
? "No number was selected."
: SelectedNumber + " was selected.);
With nullable types (including references), you can use the ??
operator to specify a default value to be used if the object is null. So control.IsChecked != null && control.IsChecked
could be replaced by control.IsChecked ?? false
. This doesn't solve all your problems, but it helps to reduce the amount of code you type in some cases.
A less verbose form is
control != null && control.IsChecked == true
Remember, a bool?
has three values, true, false, and null, and it's always sufficient to check against a single value. For example, a == true
and a != false
are the checks, respectively, for when the null works like a false or when the null works like a true.
For your combobox example, I'd use a strongly typed collection to begin with. See wpf: how to make ComboBoxItems hold integers in xaml for an example of how to bind directly to integers, (or if you want separate Content/Value, bind it to a list of KeyValuePairs [for example]) Then use SelectedValue and SelectedValuePath to reduce your value-retrieval code.
精彩评论