// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace System.Windows.Controls.DataVisualization.Charting { /// /// Represents a control that displays a data point. /// /// Preview [TemplateVisualState(Name = DataPoint.StateCommonNormal, GroupName = DataPoint.GroupCommonStates)] [TemplateVisualState(Name = DataPoint.StateCommonMouseOver, GroupName = DataPoint.GroupCommonStates)] [TemplateVisualState(Name = DataPoint.StateSelectionUnselected, GroupName = DataPoint.GroupSelectionStates)] [TemplateVisualState(Name = DataPoint.StateSelectionSelected, GroupName = DataPoint.GroupSelectionStates)] [TemplateVisualState(Name = DataPoint.StateRevealShown, GroupName = DataPoint.GroupRevealStates)] [TemplateVisualState(Name = DataPoint.StateRevealHidden, GroupName = DataPoint.GroupRevealStates)] public abstract partial class DataPoint : Control { #region CommonStates /// /// Common state group. /// internal const string GroupCommonStates = "CommonStates"; /// /// Normal state of the Common group. /// internal const string StateCommonNormal = "Normal"; /// /// MouseOver state of the Common group. /// internal const string StateCommonMouseOver = "MouseOver"; #endregion CommonStates #region SelectionStates /// /// Selection state group. /// internal const string GroupSelectionStates = "SelectionStates"; /// /// Unselected state of the Selection group. /// internal const string StateSelectionUnselected = "Unselected"; /// /// Selected state of the Selection group. /// internal const string StateSelectionSelected = "Selected"; #endregion SelectionStates #region GroupRevealStates /// /// Reveal state group. /// internal const string GroupRevealStates = "RevealStates"; /// /// Shown state of the Reveal group. /// internal const string StateRevealShown = "Shown"; /// /// Hidden state of the Reveal group. /// internal const string StateRevealHidden = "Hidden"; #endregion GroupRevealStates #region public bool IsSelectionEnabled /// /// Gets or sets a value indicating whether selection is enabled. /// public bool IsSelectionEnabled { get { return (bool)GetValue(IsSelectionEnabledProperty); } set { SetValue(IsSelectionEnabledProperty, value); } } /// /// Identifies the IsSelectionEnabled dependency property. /// public static readonly DependencyProperty IsSelectionEnabledProperty = DependencyProperty.Register( "IsSelectionEnabled", typeof(bool), typeof(DataPoint), new PropertyMetadata(false, OnIsSelectionEnabledPropertyChanged)); /// /// IsSelectionEnabledProperty property changed handler. /// /// Control that changed its IsSelectionEnabled. /// Event arguments. private static void OnIsSelectionEnabledPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; source.OnIsSelectionEnabledPropertyChanged(oldValue, newValue); } /// /// IsSelectionEnabledProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnIsSelectionEnabledPropertyChanged(bool oldValue, bool newValue) { if (newValue == false) { IsSelected = false; IsHovered = false; } } #endregion public bool IsSelectionEnabled /// /// Gets a value indicating whether the data point is active. /// internal bool IsActive { get; private set; } /// /// An event raised when the IsSelected property is changed. /// internal event RoutedPropertyChangedEventHandler IsSelectedChanged; /// /// A value indicating whether the mouse is hovering over the data /// point. /// private bool _isHovered; /// /// Gets a value indicating whether the mouse is hovering over /// the data point. /// protected bool IsHovered { get { return _isHovered; } private set { bool oldValue = _isHovered; _isHovered = value; if (oldValue != _isHovered) { OnIsHoveredPropertyChanged(oldValue, value); } } } /// /// IsHoveredProperty property changed handler. /// /// Old value. /// New value. protected virtual void OnIsHoveredPropertyChanged(bool oldValue, bool newValue) { VisualStateManager.GoToState(this, (newValue == true) ? StateCommonMouseOver : StateCommonNormal, true); } #region internal bool IsSelected /// /// Gets or sets a value indicating whether the data point is selected. /// internal bool IsSelected { get { return (bool)GetValue(IsSelectedProperty); } set { SetValue(IsSelectedProperty, value); } } /// /// Identifies the IsSelected dependency property. /// internal static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register( "IsSelected", typeof(bool), typeof(DataPoint), new PropertyMetadata(false, OnIsSelectedPropertyChanged)); /// /// IsSelectedProperty property changed handler. /// /// Control that changed its IsSelected. /// Event arguments. private static void OnIsSelectedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; source.OnIsSelectedPropertyChanged(oldValue, newValue); } /// /// IsSelectedProperty property changed handler. /// /// The value to be replaced. /// The new value. protected virtual void OnIsSelectedPropertyChanged(bool oldValue, bool newValue) { VisualStateManager.GoToState(this, newValue ? StateSelectionSelected : StateSelectionUnselected, true); RoutedPropertyChangedEventHandler handler = this.IsSelectedChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } } #endregion internal bool IsSelected /// /// Event raised when the actual dependent value of the data point is changed. /// internal event RoutedPropertyChangedEventHandler ActualDependentValueChanged; #region public IComparable ActualDependentValue /// /// Gets or sets the actual dependent value displayed in the chart. /// public IComparable ActualDependentValue { get { return (IComparable)GetValue(ActualDependentValueProperty); } set { SetValue(ActualDependentValueProperty, value); } } /// /// Identifies the ActualDependentValue dependency property. /// public static readonly System.Windows.DependencyProperty ActualDependentValueProperty = System.Windows.DependencyProperty.Register( "ActualDependentValue", typeof(IComparable), typeof(DataPoint), new System.Windows.PropertyMetadata(0.0, OnActualDependentValuePropertyChanged)); /// /// Called when the value of the ActualDependentValue property changes. /// /// Control that changed its ActualDependentValue. /// Event arguments. private static void OnActualDependentValuePropertyChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; IComparable oldValue = (IComparable)e.OldValue; IComparable newValue = (IComparable)e.NewValue; source.OnActualDependentValuePropertyChanged(oldValue, newValue); } /// /// A value indicating whether the actual independent value is being /// coerced. /// private bool _isCoercingActualDependentValue; /// /// The preserved previous actual dependent value before coercion. /// private IComparable _oldActualDependentValueBeforeCoercion; /// /// Called when the value of the ActualDependentValue property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnActualDependentValuePropertyChanged(IComparable oldValue, IComparable newValue) { double coercedValue = 0.0; if (!(newValue is double) && ValueHelper.TryConvert(newValue, out coercedValue)) { _isCoercingActualDependentValue = true; _oldActualDependentValueBeforeCoercion = oldValue; } if (!_isCoercingActualDependentValue) { if (_oldActualDependentValueBeforeCoercion != null) { oldValue = _oldActualDependentValueBeforeCoercion; _oldActualDependentValueBeforeCoercion = null; } RoutedPropertyChangedEventHandler handler = this.ActualDependentValueChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } } if (_isCoercingActualDependentValue) { _isCoercingActualDependentValue = false; this.ActualDependentValue = coercedValue; } } #endregion public IComparable ActualDependentValue /// /// This event is raised when the dependent value of the data point is /// changed. /// internal event RoutedPropertyChangedEventHandler DependentValueChanged; #region public IComparable DependentValue /// /// Gets or sets the dependent value of the Control. /// public IComparable DependentValue { get { return (IComparable) GetValue(DependentValueProperty); } set { SetValue(DependentValueProperty, value); } } /// /// Identifies the DependentValue dependency property. /// public static readonly DependencyProperty DependentValueProperty = DependencyProperty.Register( "DependentValue", typeof(IComparable), typeof(DataPoint), new PropertyMetadata(null, OnDependentValuePropertyChanged)); /// /// Called when the DependentValue property changes. /// /// Control that changed its DependentValue. /// Event arguments. private static void OnDependentValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; IComparable oldValue = (IComparable) e.OldValue; IComparable newValue = (IComparable) e.NewValue; source.OnDependentValuePropertyChanged(oldValue, newValue); } /// /// Called when the DependentValue property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnDependentValuePropertyChanged(IComparable oldValue, IComparable newValue) { SetFormattedProperty(FormattedDependentValueProperty, DependentValueStringFormat, newValue); RoutedPropertyChangedEventHandler handler = this.DependentValueChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } if (this.State == DataPointState.Created) { // Prefer setting the value as a double... double coercedNewValue; if (ValueHelper.TryConvert(newValue, out coercedNewValue)) { ActualDependentValue = coercedNewValue; } else { // ... but fall back otherwise ActualDependentValue = newValue; } } } #endregion public IComparable DependentValue #region public string DependentValueStringFormat /// /// Gets or sets the format string for the FormattedDependentValue property. /// public string DependentValueStringFormat { get { return GetValue(DependentValueStringFormatProperty) as string; } set { SetValue(DependentValueStringFormatProperty, value); } } /// /// Identifies the DependentValueStringFormat dependency property. /// public static readonly DependencyProperty DependentValueStringFormatProperty = DependencyProperty.Register( "DependentValueStringFormat", typeof(string), typeof(DataPoint), new PropertyMetadata(null, OnDependentValueStringFormatPropertyChanged)); /// /// Called when DependentValueStringFormat property changes. /// /// Control that changed its DependentValueStringFormat. /// Event arguments. private static void OnDependentValueStringFormatPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = d as DataPoint; string oldValue = e.OldValue as string; string newValue = e.NewValue as string; source.OnDependentValueStringFormatPropertyChanged(oldValue, newValue); } /// /// Called when DependentValueStringFormat property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnDependentValueStringFormatPropertyChanged(string oldValue, string newValue) { SetFormattedProperty(FormattedDependentValueProperty, newValue, DependentValue); } #endregion public string DependentValueStringFormat #region public string FormattedDependentValue /// /// Gets the DependentValue as formatted by the DependentValueStringFormat property. /// public string FormattedDependentValue { get { return GetValue(FormattedDependentValueProperty) as string; } } /// /// Identifies the FormattedDependentValue dependency property. /// public static readonly DependencyProperty FormattedDependentValueProperty = DependencyProperty.Register( "FormattedDependentValue", typeof(string), typeof(DataPoint), null); #endregion public string FormattedDependentValue #region public string FormattedIndependentValue /// /// Gets the IndependentValue as formatted by the IndependentValueStringFormat property. /// public string FormattedIndependentValue { get { return GetValue(FormattedIndependentValueProperty) as string; } } /// /// Identifies the FormattedIndependentValue dependency property. /// public static readonly DependencyProperty FormattedIndependentValueProperty = DependencyProperty.Register( "FormattedIndependentValue", typeof(string), typeof(DataPoint), null); #endregion public string FormattedIndependentValue /// /// Called when the independent value of the data point is changed. /// internal event RoutedPropertyChangedEventHandler IndependentValueChanged; #region public object IndependentValue /// /// Gets or sets the independent value. /// public object IndependentValue { get { return GetValue(IndependentValueProperty); } set { SetValue(IndependentValueProperty, value); } } /// /// Identifies the IndependentValue dependency property. /// public static readonly DependencyProperty IndependentValueProperty = DependencyProperty.Register( "IndependentValue", typeof(object), typeof(DataPoint), new PropertyMetadata(null, OnIndependentValuePropertyChanged)); /// /// Called when the IndependentValue property changes. /// /// Control that changed its IndependentValue. /// Event arguments. private static void OnIndependentValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; object oldValue = e.OldValue; object newValue = e.NewValue; source.OnIndependentValuePropertyChanged(oldValue, newValue); } /// /// Called when the IndependentValue property changes. /// /// The old value. /// The new value. protected virtual void OnIndependentValuePropertyChanged(object oldValue, object newValue) { SetFormattedProperty(FormattedIndependentValueProperty, IndependentValueStringFormat, newValue); RoutedPropertyChangedEventHandler handler = this.IndependentValueChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } if (this.State == DataPointState.Created) { // Prefer setting the value as a double... double coercedNewValue; if (ValueHelper.TryConvert(newValue, out coercedNewValue)) { ActualIndependentValue = coercedNewValue; } else { // ... but fall back otherwise ActualIndependentValue = newValue; } } } #endregion public object IndependentValue #region public string IndependentValueStringFormat /// /// Gets or sets the format string for the FormattedIndependentValue property. /// public string IndependentValueStringFormat { get { return GetValue(IndependentValueStringFormatProperty) as string; } set { SetValue(IndependentValueStringFormatProperty, value); } } /// /// Identifies the IndependentValueStringFormat dependency property. /// public static readonly DependencyProperty IndependentValueStringFormatProperty = DependencyProperty.Register( "IndependentValueStringFormat", typeof(string), typeof(DataPoint), new PropertyMetadata(null, OnIndependentValueStringFormatPropertyChanged)); /// /// Called when the value of the IndependentValueStringFormat property changes. /// /// Control that changed its IndependentValueStringFormat. /// Event arguments. private static void OnIndependentValueStringFormatPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = d as DataPoint; string oldValue = e.OldValue as string; string newValue = e.NewValue as string; source.OnIndependentValueStringFormatPropertyChanged(oldValue, newValue); } /// /// Called when the value of the IndependentValueStringFormat property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnIndependentValueStringFormatPropertyChanged(string oldValue, string newValue) { SetFormattedProperty(FormattedIndependentValueProperty, newValue, IndependentValue); } #endregion public string IndependentValueStringFormat /// /// Occurs when the actual independent value of the data point is /// changed. /// internal event RoutedPropertyChangedEventHandler ActualIndependentValueChanged; #region public object ActualIndependentValue /// /// Gets or sets the actual independent value. /// public object ActualIndependentValue { get { return (object)GetValue(ActualIndependentValueProperty); } set { SetValue(ActualIndependentValueProperty, value); } } /// /// A value indicating whether the actual independent value is being /// coerced. /// private bool _isCoercingActualIndependentValue; /// /// The preserved previous actual dependent value before coercion. /// private object _oldActualIndependentValueBeforeCoercion; /// /// Identifies the ActualIndependentValue dependency property. /// public static readonly DependencyProperty ActualIndependentValueProperty = DependencyProperty.Register( "ActualIndependentValue", typeof(object), typeof(DataPoint), new PropertyMetadata(OnActualIndependentValuePropertyChanged)); /// /// Called when the ActualIndependentValue property changes. /// /// Control that changed its ActualIndependentValue. /// Event arguments. private static void OnActualIndependentValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; object oldValue = (object)e.OldValue; object newValue = (object)e.NewValue; source.OnActualIndependentValuePropertyChanged(oldValue, newValue); } /// /// Called when the ActualIndependentValue property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnActualIndependentValuePropertyChanged(object oldValue, object newValue) { double coercedValue = 0.0; if (!(newValue is double) && ValueHelper.TryConvert(newValue, out coercedValue)) { _isCoercingActualIndependentValue = true; _oldActualIndependentValueBeforeCoercion = oldValue; } if (!_isCoercingActualIndependentValue) { if (_oldActualIndependentValueBeforeCoercion != null) { oldValue = _oldActualIndependentValueBeforeCoercion; _oldActualIndependentValueBeforeCoercion = null; } RoutedPropertyChangedEventHandler handler = this.ActualIndependentValueChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } } if (_isCoercingActualIndependentValue) { _isCoercingActualIndependentValue = false; this.ActualIndependentValue = coercedValue; } } #endregion public object ActualIndependentValue /// /// Occurs when the state of a data point is changed. /// internal event RoutedPropertyChangedEventHandler StateChanged; #region public DataPointState State /// /// Gets or sets a value indicating whether the State property is being /// coerced to its previous value. /// private bool IsCoercingState { get; set; } /// /// Gets or sets the state of the data point. /// internal DataPointState State { get { return (DataPointState)GetValue(StateProperty); } set { SetValue(StateProperty, value); } } /// /// Identifies the State dependency property. /// internal static readonly DependencyProperty StateProperty = DependencyProperty.Register( "State", typeof(DataPointState), typeof(DataPoint), new PropertyMetadata(DataPointState.Created, OnStatePropertyChanged)); /// /// Called when the value of the State property changes. /// /// Control that changed its State. /// Event arguments. private static void OnStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { DataPoint source = (DataPoint)d; DataPointState oldValue = (DataPointState)e.OldValue; DataPointState newValue = (DataPointState)e.NewValue; source.OnStatePropertyChanged(oldValue, newValue); } /// /// Called when the value of the State property changes. /// /// The value to be replaced. /// The new value. protected virtual void OnStatePropertyChanged(DataPointState oldValue, DataPointState newValue) { if (!IsCoercingState) { // If state ever goes to or past PendingRemoval, the DataPoint is no longer active if (DataPointState.PendingRemoval <= newValue) { IsActive = false; } if (newValue < oldValue) { // If we've somehow gone backwards in the life cycle (other // than when we go back to normal from updating) coerce to // old value. IsCoercingState = true; this.State = oldValue; IsCoercingState = false; } else { // Update selection if (newValue > DataPointState.Normal) { this.IsSelectionEnabled = false; } // Start state transition bool transitionStarted = false; switch (newValue) { case DataPointState.Showing: case DataPointState.Hiding: transitionStarted = GoToCurrentRevealState(); break; } // Fire Changed event RoutedPropertyChangedEventHandler handler = this.StateChanged; if (handler != null) { handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue)); } // Change state if no transition started if (!transitionStarted && _templateApplied) { switch (newValue) { case DataPointState.Showing: State = DataPointState.Normal; break; case DataPointState.Hiding: State = DataPointState.Hidden; break; } } } } } #endregion internal DataPointState State /// /// Gets the implementation root of the Control. /// /// /// Implements Silverlight's corresponding internal property on Control. /// private FrameworkElement ImplementationRoot { get { return (1 == VisualTreeHelper.GetChildrenCount(this)) ? VisualTreeHelper.GetChild(this, 0) as FrameworkElement : null; } } /// /// Tracks whether the Reveal/Shown VisualState is available. /// private bool _haveStateRevealShown; /// /// Tracks whether the Reveal/Hidden VisualState is available. /// private bool _haveStateRevealHidden; /// /// Tracks whether the template has been applied yet. /// private bool _templateApplied; /// /// Initializes a new instance of the DataPoint class. /// protected DataPoint() { Loaded += new RoutedEventHandler(OnLoaded); IsActive = true; } /// /// Updates the Control's visuals to reflect the current state(s). /// /// True if a state transition was started. private bool GoToCurrentRevealState() { bool transitionStarted = false; string stateName = null; switch (State) { case DataPointState.Showing: if (_haveStateRevealShown) { stateName = StateRevealShown; } break; case DataPointState.Hiding: if (_haveStateRevealHidden) { stateName = StateRevealHidden; } break; } if (null != stateName) { if (!DesignerProperties.GetIsInDesignMode(this)) { // The use of Dispatcher.BeginInvoke here is necessary to // work around the StackOverflowException Silverlight throws // when it tries to play too many VSM animations. Dispatcher.BeginInvoke(new Action(() => VisualStateManager.GoToState(this, stateName, true))); } else { VisualStateManager.GoToState(this, stateName, false); } transitionStarted = true; } return transitionStarted; } /// /// Builds the visual tree for the DataPoint when a new template is applied. /// public override void OnApplyTemplate() { // Unhook CurrentStateChanged handler VisualStateGroup groupReveal = VisualStateManager.GetVisualStateGroups(ImplementationRoot).CastWrapper().Where(group => GroupRevealStates == group.Name).FirstOrDefault(); if (null != groupReveal) { groupReveal.CurrentStateChanged -= new EventHandler(OnCurrentStateChanged); } base.OnApplyTemplate(); // Hook CurrentStateChanged handler _haveStateRevealShown = false; _haveStateRevealHidden = false; groupReveal = VisualStateManager.GetVisualStateGroups(ImplementationRoot).CastWrapper().Where(group => GroupRevealStates == group.Name).FirstOrDefault(); if (null != groupReveal) { groupReveal.CurrentStateChanged += new EventHandler(OnCurrentStateChanged); _haveStateRevealShown = groupReveal.States.CastWrapper().Where(state => StateRevealShown == state.Name).Any(); _haveStateRevealHidden = groupReveal.States.CastWrapper().Where(state => StateRevealHidden == state.Name).Any(); } _templateApplied = true; // Go to current state(s) GoToCurrentRevealState(); if (DesignerProperties.GetIsInDesignMode(this)) { // Transition to Showing state in design mode so DataPoint will be visible State = DataPointState.Showing; } } /// /// Changes the DataPoint object's state after one of the VSM state animations completes. /// /// Event source. /// Event arguments. private void OnCurrentStateChanged(object sender, VisualStateChangedEventArgs e) { switch (e.NewState.Name) { case StateRevealShown: if (State == DataPointState.Showing) { State = DataPointState.Normal; } break; case StateRevealHidden: if (State == DataPointState.Hiding) { State = DataPointState.Hidden; } break; } } /// /// Handles the Control's Loaded event. /// /// The Control. /// Event arguments. private void OnLoaded(object sender, RoutedEventArgs e) { GoToCurrentRevealState(); } /// /// Provides handling for the MouseEnter event. /// /// Event arguments. protected override void OnMouseEnter(MouseEventArgs e) { base.OnMouseEnter(e); if (IsSelectionEnabled) { IsHovered = true; } } /// /// Provides handling for the MouseLeave event. /// /// Event arguments. protected override void OnMouseLeave(MouseEventArgs e) { base.OnMouseLeave(e); if (IsSelectionEnabled) { IsHovered = false; } } /// /// Provides handling for the MouseLeftButtonDown event. /// /// Event arguments. protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { if (DefinitionSeriesIsSelectionEnabledHandling) { // DefinitionSeries-compatible handling if (!IsSelectionEnabled) { // Prevents clicks from bubbling to background, but necessary // to avoid letting ListBoxItem select the item e.Handled = true; } base.OnMouseLeftButtonDown(e); } else { // Traditional handling base.OnMouseLeftButtonDown(e); if (IsSelectionEnabled) { IsSelected = (ModifierKeys.None == (ModifierKeys.Control & Keyboard.Modifiers)); e.Handled = true; } } } /// /// Gets or sets a value indicating whether to handle IsSelectionEnabled in the DefinitionSeries manner. /// internal bool DefinitionSeriesIsSelectionEnabledHandling { get; set; } /// /// Sets a dependency property with the specified format. /// /// The DependencyProperty to set. /// The Format string to apply to the value. /// The value of the dependency property to be formatted. internal void SetFormattedProperty(DependencyProperty property, string format, object value) { SetValue(property, string.Format(CultureInfo.CurrentCulture, format ?? "{0}", value)); } } }