Udostępnij za pośrednictwem


Property Usage Guidelines

Determine whether a property or a method is more appropriate for your needs. For details on choosing between properties and methods, see Properties vs. Methods.

Choose a name for your property based on the recommended Property Naming Guidelines.

When accessing a property using the set accessor, preserve the value of the property before you change it. This will ensure that data is not lost if the set accessor throws an exception.

Property State Issues

Allow properties to be set in any order. Properties should be stateless with respect to other properties. It is often the case that a particular feature of an object will not take effect until the developer specifies a particular set of properties, or until an object has a particular state. Until the object is in the correct state, the feature is not active. When the object is in the correct state, the feature automatically activates itself without requiring an explicit call. The semantics are the same regardless of the order in which the developer sets the property values or how the developer gets the object into the active state.

For example, a TextBox control might have two related properties: DataSource and DataField. DataSource specifies the table name, and DataField specifies the column name. Once both properties are specified, the control can automatically bind data from the table into the Text property of the control. The following code example illustrates properties that can be set in any order.

Dim t As New TextBox()
t.DataSource = "Publishers"
t.DataField = "AuthorID"
' The data-binding feature is now active.
[C#]
TextBox t = new TextBox();
t.DataSource = "Publishers";
t.DataField = "AuthorID";
// The data-binding feature is now active.

You can set the DataSource and DataField properties in any order. Therefore, the preceding code is equivalent to the following.

Dim t As New TextBox()
t.DataField = "AuthorID"
t.DataSource = "Publishers"
' The data-binding feature is now active.

[C#]
TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.

You can also set a property to null (Nothing in Visual Basic) to indicate that the value is not specified.

Dim t As New TextBox()
t.DataField = "AuthorID"
t.DataSource = "Publishers"
' The data-binding feature is now active.
t.DataSource = Nothing
' The data-binding feature is now inactive.
[C#]
TextBox t = new TextBox();
t.DataField = "AuthorID";
t.DataSource = "Publishers";
// The data-binding feature is now active.
t.DataSource = null;
// The data-binding feature is now inactive.

The following code example illustrates how to track the state of the data binding feature and automatically activate or deactivate it at the appropriate times.

Public Class TextBox
   Private m_dataSource As String
   Private m_dataField As String
   Private m_active As Boolean

   Public Property DataSource() As String
      Get
         Return m_dataSource
      End Get
      Set
         If value <> m_dataSource Then
            ' Set the property value first, in case activate fails.
            m_dataSource = value
            ' Update active state.
            SetActive(( Not (m_dataSource Is Nothing) And Not (m_dataField Is Nothing)))
         End If
      End Set
   End Property
   Public Property DataField() As String
      Get
         Return m_dataField
      End Get
      Set
         If value <> m_dataField Then
            ' Set the property value first, in case activate fails.
            m_dataField = value
            ' Update active state.
            SetActive(( Not (m_dataSource Is Nothing) And Not (m_dataField Is Nothing)))
         End If
      End Set
   End Property
   Sub SetActive(m_value As Boolean)
      If value <> m_active Then
         If m_value Then
            Activate()
            Text = dataBase.Value(m_dataField)
         Else
            Deactivate()
            Text = ""
         End If
         ' Set active only if successful.
         m_active = value
      End If
   End Sub
   Sub Activate()
      ' Open database.
   End Sub
 
   Sub Deactivate()
      ' Close database.
   End Sub
End Class
[C#]
public class TextBox
{
   string dataSource;
   string dataField;
   bool active;

   public string DataSource
   {   
      get
      {
         return dataSource;
      }
      set
      {
         if (value != dataSource)
         {
            // Update active state.
            SetActive(value != null && dataField != null);
            dataSource = value;
         }
      }
      }
      
   public string DataField
   {
      get
      {
         return dataField;
      }
      set
      {
         if (value != dataField)
         {
            // Update active state.
            SetActive(dataSource != null && dataField != null);
            dataField = value;
         }
      }
   }   
   void SetActive(Boolean value)
   {
      if (value != active)
      {
         if (value)
         {
            Activate();
            Text = dataBase.Value(dataField);
         }
         else
         {
            Deactivate();
            Text = "";
         }
         // Set active only if successful.
         active = value; 
      }
   }
   void Activate()
   {
      // Open database.
   }
      
   void Deactivate()
   {
      // Close database.
   }
}

In the preceding example, the following expression determines whether the object is in a state in which the data-binding feature can activate itself.

(Not (value Is Nothing) And Not (m_dataField Is Nothing))
[C#]
value != null && dataField != null

You make activation automatic by creating a method that determines whether the object can be activated given its current state, and then activates it as necessary.

Sub UpdateActive(m_dataSource As String, m_dataField As String)
   SetActive(( Not (m_dataSource Is Nothing) And Not (m_dataField Is Nothing)))
End Sub
[C#]
void UpdateActive(string dataSource, string dataField)
{
   SetActive(dataSource != null && dataField != null);
}

If you do have related properties, such as DataSource and DataMember, you should consider implementing the ISupportInitialize Interface. This will allow the designer (or user) to call the ISupportInitialize.BeginInit and ISupportInitialize.EndInit methods when setting multiple properties to allow the component to provide optimizations. In the above example, ISupportInitialize could prevent unnecessary attempts to access the database until setup is correctly completed.

The expression that appears in this method indicates the parts of the object model that need to be examined in order to enforce these state transitions. In this case, the DataSource and DataField properties are affected. For more information on choosing between properties and methods, see Properties vs. Methods.

Raising Property-Changed Events

Components should raise property-changed events if they want to notify consumers when the component's property changes programmatically. The naming convention for a property-changed event is to add the Changed suffix to the property name, such as TextChanged. For example, a control might raise a TextChanged event when its text property changes. You can use a protected helper routine Raise<Property>Changed, to raise this event. However, it is probably not worth the overhead to raise a property-changed event for a hash table item addition. The following code example illustrates the implementation of a helper routine on a property-changed event.

Class Control
   Inherits Component
   Private m_text As String
   Public Property Text() As String
      Get
         Return m_text
      End Get
      Set
         If Not m_text.Equals(value) Then
            m_text = value
            RaiseTextChanged()
         End If
      End Set
   End Property
End Class
[C#]
class Control: Component
{
   string text;
   public string Text
   { 
      get
      { 
         return text; 
      }
      set
      {
         if (!text.Equals(value))
         {
            text = value;
            RaiseTextChanged();
         }
      }
   }
}

Data binding uses this pattern to allow two-way binding of the property. Without <Property>Changed and Raise<Property>Changed events, data binding works in one direction; if the database changes, the property is updated. Each property that raises the <Property>Changed event should provide metadata to indicate that the property supports data binding.

It is recommended that you raise changing/changed events if the value of a property changes as a result of external forces. These events indicate to the developer that the value of a property is changing or has changed as a result of an operation, rather than by calling methods on the object.

A good example is the Text property of an Edit control. As a user types information into the control, the property value automatically changes. An event is raised before the value of the property has changed. It does not pass the old or new value, and the developer can cancel the event by throwing an exception. The name of the event is the name of the property followed by the suffix Changing. The following code example illustrates a changing event.

Class Edit
   Inherits Control
   
   Public Property Text() As String
      Get
         Return m_text
      End Get
      Set
         If m_text <> value Then
            OnTextChanging(Event.Empty)
            m_text = value
         End If
      End Set
   End Property
End Class
[C#]
class Edit : Control 
{
   public string Text 
   { 
      get 
      { 
         return text; 
      }
      set 
      {
         if (text != value) 
         {
            OnTextChanging(Event.Empty);
            text = value;
         }
      }
   }
}

An event is also raised after the value of the property has changed. This event cannot be canceled. The name of the event is the name of the property followed by the suffix Changed. The generic PropertyChanged event should also be raised. The pattern for raising both of these events is to raise the specific event from the OnPropertyChanged method. The following example illustrates the use of the OnPropertyChanged method.

Class Edit
   Inherits Control  
   Public Property Text() As String
      Get
         Return m_text
      End Get
      Set
         If m_text <> value Then
            OnTextChanging(Event.Empty)
            m_text = value
            RaisePropertyChangedEvent(Edit.ClassInfo. m_text)
         End If
      End Set
   End Property
   Protected Sub OnPropertyChanged(e As PropertyChangedEventArgs)
      If e.PropertyChanged.Equals(Edit.ClassInfo. m_text) Then
         OnTextChanged(Event.Empty)
      End If
      If Not (onPropertyChangedHandler Is Nothing) Then
         onPropertyChangedHandler(Me, e)
      End If
   End Sub
End Class
[C#]
class Edit : Control 
{
   public string Text 
   {
      get 
      { 
         return text; 
      }
      set 
      {
         if (text != value) 
         {
            OnTextChanging(Event.Empty);
            text = value;
            RaisePropertyChangedEvent(Edit.ClassInfo.text);
         }
      }
   }

   protected void OnPropertyChanged(PropertyChangedEventArgs e) 
   {
      if (e.PropertyChanged.Equals(Edit.ClassInfo.text))
         OnTextChanged(Event.Empty);
      if (onPropertyChangedHandler != null)
         onPropertyChangedHandler(this, e);
   }
}

There are cases when the underlying value of a property is not stored as a field, making it difficult to track changes to the value. When raising the changing event, find all the places that the property value can change and provide the ability to cancel the event. For example, the previous Edit control example is not entirely accurate because the Text value is actually stored in the window handle (HWND). In order to raise the TextChanging event, you must examine Windows messages to determine when the text might change, and allow for an exception thrown in OnTextChanging to cancel the event. If it is too difficult to provide a changing event, it is reasonable to support only the changed event.

Properties vs. Methods

Class library designers often must decide between implementing a class member as a property or a method. In general, methods represent actions and properties represent data. Use the following guidelines to help you choose between these options.

  • Use a property when the member is a logical data member. In the following member declarations, Name is a property because it is a logical member of the class.

    Public Property Name As String
       Get
          Return m_name
       End Get
       Set
          m_name = value
       End Set 
    End Property
    [C#]
    public string Name
       get 
       {
          return name;
       }
       set 
       {
          name = value;
       }
    
  • Use a method when:

    • The operation is a conversion, such as Object.ToString.

    • The operation is expensive enough that you want to communicate to the user that they should consider caching the result.

    • Obtaining a property value using the get accessor would have an observable side effect.

    • Calling the member twice in succession produces different results.

    • The order of execution is important. Note that a type's properties should be able to be set and retrieved in any order.

    • The member is static but returns a value that can be changed.

    • The member returns an array. Properties that return arrays can be very misleading. Usually it is necessary to return a copy of the internal array so that the user cannot change internal state. This, coupled with the fact that a user can easily assume it is an indexed property, leads to inefficient code. In the following code example, each call to the Methods property creates a copy of the array. As a result, 2n+1 copies of the array will be created in the following loop.

      Dim type As Type = ' Get a type.
      Dim i As Integer
      For i = 0 To type.Methods.Length - 1 
         If type.Methods(i).Name.Equals("text") Then
            ' Perform some operation.
         End If 
      Next i
      [C#]
      Type type = // Get a type.
      for (int i = 0; i < type.Methods.Length; i++)
      {
         if (type.Methods[i].Name.Equals ("text"))
         {
            // Perform some operation.
         }
      }
      

The following example illustrates the correct use of properties and methods.

Class Connection
   ' The following three members should be properties
   ' because they can be set in any order.   
   Property DNSName() As String
      ' Code for get and set accessors goes here.
   End Property
   Property UserName() As String
      ' Code for get and set accessors goes here.
   End Property
   Property Password() As String
      'Code for get and set accessors goes here.
   End Property
   ' The following member should be a method
   ' because the order of execution is important.
   ' This method cannot be executed until after the 
   ' properties have been set.   
   Function Execute() As Boolean
[C#]
class Connection
{
   // The following three members should be properties
   // because they can be set in any order.
   string DNSName {get{};set{};}
   string UserName {get{};set{};}
   string Password {get{};set{};}

   // The following member should be a method
   // because the order of execution is important.
   // This method cannot be executed until after the 
   // properties have been set.
   bool Execute ();
}

Read-Only and Write-Only Properties

You should use a read-only property when the user cannot change the property's logical data member. Do not use write-only properties.

Indexed Property Usage

Note   An indexed property can also be referred to as an indexer.

The following rules outline guidelines for using indexed properties:

  • Use an indexed property when the property's logical data member is an array.

  • Consider using only integral values or strings for an indexed property. If the design requires other types for the indexed property, reconsider whether it represents a logical data member. If not, use a method.

  • Consider using only one index. If the design requires multiple indexes, reconsider whether it represents a logical data member. If not, use a method.

  • Use only one indexed property per class, and make it the default indexed property for that class. This rule is enforced by indexer support in the C# programming language.

  • Do not use nondefault indexed properties. C# does not allow this.

  • Name an indexed property Item. For example, see the DataGrid.Item Property. Follow this rule, unless there is a name that is more obvious to users, such as the Chars property on the String class. In C#, indexers are always named Item.

  • Do not provide an indexed property and a method that are semantically equivalent to two or more overloaded methods. In the following code example, the Method property should be changed to GetMethod(string) method. Note that this not allowed in C#.

    ' Change the MethodInfo Type.Method property to a method.
    Property Type.Method(name As String) As MethodInfo
    Function Type.GetMethod(name As String, ignoreCase As Boolean) As MethodInfo
    [C#]
    // Change the MethodInfo Type.Method property to a method.
    MethodInfo Type.Method[string name]
    MethodInfo Type.GetMethod (string name, Boolean ignoreCase)
    [Visual Basic]
    ' The MethodInfo Type.Method property is changed to
    ' the MethodInfo Type.GetMethod method.
    Function Type.GetMethod(name As String) As MethodInfo
    Function Type.GetMethod(name As String, ignoreCase As Boolean) As MethodInfo
    [C#]
    // The MethodInfo Type.Method property is changed to
    // the MethodInfo Type.GetMethod method.
    MethodInfo Type.GetMethod(string name)
    MethodInfo Type.GetMethod (string name, Boolean ignoreCase) 
    

See Also

Design Guidelines for Class Library Developers | Property Naming Guidelines | Class Member Usage Guidelines