// (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.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows.Automation;
using System.Windows.Automation.Peers;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media.Animation;
namespace System.Windows.Controls
{
///
/// A control that has a rating.
///
/// Preview
[TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateMouseOver, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StatePressed, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateDisabled, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateReadOnly, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateFocused, GroupName = VisualStates.GroupFocus)]
[TemplateVisualState(Name = VisualStates.StateUnfocused, GroupName = VisualStates.GroupFocus)]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(RatingItem))]
public class Rating : ItemsControl, IUpdateVisualState
{
#region protected double DisplayValue
///
/// Gets or sets the actual value of the Rating control.
///
protected double DisplayValue
{
get { return (double)GetValue(DisplayValueProperty); }
set { SetValue(DisplayValueProperty, value); }
}
///
/// Identifies the DisplayValue dependency property.
///
protected static readonly DependencyProperty DisplayValueProperty =
DependencyProperty.Register(
"DisplayValue",
typeof(double),
typeof(Rating),
new PropertyMetadata(0.0, OnDisplayValueChanged));
///
/// DisplayValueProperty property changed handler.
///
/// Rating that changed its DisplayValue.
/// Event arguments.
private static void OnDisplayValueChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs eventArgs)
{
Rating source = (Rating)dependencyObject;
source.OnDisplayValueChanged();
}
///
/// DisplayValueProperty property changed handler.
///
private void OnDisplayValueChanged()
{
UpdateDisplayValues();
}
#endregion protected double DisplayValue
///
/// Gets or sets the rating item hovered over.
///
private RatingItem HoveredRatingItem { get; set; }
///
/// Gets the helper that provides all of the standard
/// interaction functionality.
///
internal InteractionHelper Interaction { get; private set; }
#if SILVERLIGHT
///
/// Gets or sets the items control helper class.
///
private ItemsControlHelper ItemsControlHelper { get; set; }
#endif
#region public int ItemCount
///
/// Gets or sets the number of rating items.
///
public int ItemCount
{
get { return (int)GetValue(ItemCountProperty); }
set { SetValue(ItemCountProperty, value); }
}
///
/// Identifies the ItemCount dependency property.
///
public static readonly DependencyProperty ItemCountProperty =
DependencyProperty.Register(
"ItemCount",
typeof(int),
typeof(Rating),
new PropertyMetadata(0, OnItemCountChanged));
///
/// ItemCountProperty property changed handler.
///
/// Rating that changed its ItemCount.
/// Event arguments.
private static void OnItemCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Rating source = d as Rating;
int value = (int)e.NewValue;
source.OnItemCountChanged(value);
}
///
/// This method is invoked when the items count property is changed.
///
/// The new value.
private void OnItemCountChanged(int newValue)
{
if (newValue < 0)
{
throw new ArgumentException(Muchinfo.WPF.Controls.Properties.Resources.Rating_SetItemCount_ItemCountMustBeLargerThanOrEqualToZero);
}
int amountToAdd = newValue - this.Items.Count;
if (amountToAdd > 0)
{
for (int cnt = 0; cnt < amountToAdd; cnt++)
{
this.Items.Add(new RatingItem());
}
}
else if (amountToAdd < 0)
{
for (int cnt = 0; cnt < Math.Abs(amountToAdd); cnt++)
{
this.Items.RemoveAt(this.Items.Count - 1);
}
}
}
#endregion public int ItemCount
#region public bool IsReadOnly
///
/// Gets or sets a value indicating whether the Rating is read-only.
///
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
///
/// Identifies the IsReadOnly dependency property.
///
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register(
"IsReadOnly",
typeof(bool),
typeof(Rating),
new PropertyMetadata(false, OnIsReadOnlyChanged));
///
/// IsReadOnlyProperty property changed handler.
///
/// Rating that changed its IsReadOnly.
/// Event arguments.
private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Rating source = (Rating)d;
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
source.OnIsReadOnlyChanged(oldValue, newValue);
}
///
/// IsReadOnlyProperty property changed handler.
///
/// Old value.
/// New value.
protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue)
{
Interaction.OnIsReadOnlyChanged(newValue);
foreach (RatingItem ratingItem in GetRatingItems())
{
ratingItem.IsReadOnly = newValue;
}
UpdateHoverStates();
}
#endregion public bool IsReadOnly
#if SILVERLIGHT
#region public Style ItemContainerStyle
///
/// Gets or sets the item container style.
///
public Style ItemContainerStyle
{
get { return GetValue(ItemContainerStyleProperty) as Style; }
set { SetValue(ItemContainerStyleProperty, value); }
}
///
/// Identifies the ItemContainerStyle dependency property.
///
public static readonly DependencyProperty ItemContainerStyleProperty =
DependencyProperty.Register(
"ItemContainerStyle",
typeof(Style),
typeof(Rating),
new PropertyMetadata(null, OnItemContainerStyleChanged));
///
/// ItemContainerStyleProperty property changed handler.
///
/// Rating that changed its ItemContainerStyle.
/// Event arguments.
private static void OnItemContainerStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Rating source = (Rating)d;
Style newValue = (Style)e.NewValue;
source.OnItemContainerStyleChanged(newValue);
}
///
/// ItemContainerStyleProperty property changed handler.
///
/// New value.
protected virtual void OnItemContainerStyleChanged(Style newValue)
{
ItemsControlHelper.UpdateItemContainerStyle(newValue);
}
#endregion public Style ItemContainerStyle
#endif
#region public RatingSelectionMode SelectionMode
///
/// Gets or sets the selection mode.
///
public RatingSelectionMode SelectionMode
{
get { return (RatingSelectionMode) GetValue(SelectionModeProperty); }
set { SetValue(SelectionModeProperty, value); }
}
///
/// Identifies the SelectionMode dependency property.
///
public static readonly DependencyProperty SelectionModeProperty =
DependencyProperty.Register(
"SelectionMode",
typeof(RatingSelectionMode),
typeof(Rating),
new PropertyMetadata(RatingSelectionMode.Continuous, OnSelectionModeChanged));
///
/// SelectionModeProperty property changed handler.
///
/// Rating that changed its SelectionMode.
/// Event arguments.
private static void OnSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Rating source = (Rating)d;
RatingSelectionMode oldValue = (RatingSelectionMode)e.OldValue;
RatingSelectionMode newValue = (RatingSelectionMode)e.NewValue;
source.OnSelectionModeChanged(oldValue, newValue);
}
///
/// SelectionModeProperty property changed handler.
///
/// Old value.
/// New value.
protected virtual void OnSelectionModeChanged(RatingSelectionMode oldValue, RatingSelectionMode newValue)
{
UpdateDisplayValues();
}
#endregion public RatingSelectionMode SelectionMode
#region public double? Value
///
/// Gets or sets the rating value.
///
[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Value is the logical name for this property.")]
[TypeConverter(typeof(NullableConverter))]
public double? Value
{
get { return (double?)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
///
/// Identifies the Value dependency property.
///
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value",
typeof(double?),
typeof(Rating),
new PropertyMetadata(new double?(), OnValueChanged));
///
/// ValueProperty property changed handler.
///
/// Rating that changed its Value.
/// Event arguments.
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Rating source = (Rating)d;
double? oldValue = (double?)e.OldValue;
double? newValue = (double?)e.NewValue;
source.OnValueChanged(oldValue, newValue);
}
///
/// ValueProperty property changed handler.
///
/// Old value.
/// New value.
protected virtual void OnValueChanged(double? oldValue, double? newValue)
{
UpdateValues();
#if SILVERLIGHT
RoutedPropertyChangedEventHandler handler = ValueChanged;
if (handler != null)
{
handler(this, new RoutedPropertyChangedEventArgs(oldValue, newValue));
}
#else
RaiseEvent(new RoutedPropertyChangedEventArgs(oldValue, newValue, ValueChangedEvent));
#endif
}
///
/// Updates the control when the items change.
///
/// Information about the event.
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
EventHandler layoutUpdated = null;
layoutUpdated =
delegate
{
this.LayoutUpdated -= layoutUpdated;
UpdateValues();
UpdateDisplayValues();
};
this.LayoutUpdated += layoutUpdated;
this.ItemCount = this.Items.Count;
base.OnItemsChanged(e);
}
///
/// This event is raised when the value of the rating is changed.
///
#if SILVERLIGHT
public event RoutedPropertyChangedEventHandler ValueChanged;
#else
public static readonly RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(Rating));
///
/// This event is raised when the value of the rating is changed.
///
public event RoutedPropertyChangedEventHandler ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
#endif
#endregion public double? Value
#if !SILVERLIGHT
///
/// Initializes the static members of the ColumnDataPoint class.
///
static Rating()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Rating), new FrameworkPropertyMetadata(typeof(Rating)));
}
#endif
///
/// Initializes a new instance of the Rating control.
///
public Rating()
{
this.Interaction = new InteractionHelper(this);
#if SILVERLIGHT
this.DefaultStyleKey = typeof(Rating);
this.ItemsControlHelper = new ItemsControlHelper(this);
}
///
/// Applies control template to the items control.
///
public override void OnApplyTemplate()
{
ItemsControlHelper.OnApplyTemplate();
base.OnApplyTemplate();
#endif
}
///
/// This method is invoked when the mouse enters the rating item.
///
/// Information about the event.
protected override void OnMouseEnter(MouseEventArgs e)
{
if (Interaction.AllowMouseEnter(e))
{
Interaction.UpdateVisualStateBase(true);
}
base.OnMouseEnter(e);
}
///
/// This method is invoked when the mouse leaves the rating item.
///
/// Information about the event.
protected override void OnMouseLeave(MouseEventArgs e)
{
if (Interaction.AllowMouseLeave(e))
{
Interaction.UpdateVisualStateBase(true);
}
base.OnMouseLeave(e);
}
///
/// Provides handling for the Rating's MouseLeftButtonDown event.
///
/// Event arguments.
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (Interaction.AllowMouseLeftButtonDown(e))
{
Interaction.OnMouseLeftButtonDownBase();
}
base.OnMouseLeftButtonDown(e);
}
///
/// Provides handling for the Rating's MouseLeftButtonUp event.
///
/// Event arguments.
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (Interaction.AllowMouseLeftButtonUp(e))
{
Interaction.OnMouseLeftButtonUpBase();
}
base.OnMouseLeftButtonUp(e);
}
///
/// Updates the values of the rating items.
///
private void UpdateValues()
{
IList ratingItems = GetRatingItems().ToList();
RatingItem oldSelectedItem = this.GetSelectedRatingItem();
IEnumerable> itemAndWeights =
EnumerableFunctions
.Zip(
ratingItems,
ratingItems
.Select(ratingItem => 1.0)
.GetWeightedValues(Value.GetValueOrDefault()),
(item, percent) => Tuple.Create(item, percent));
foreach (Tuple itemAndWeight in itemAndWeights)
{
itemAndWeight.First.Value = itemAndWeight.Second;
}
RatingItem newSelectedItem = this.GetSelectedRatingItem();
// Notify when the selection changes
if (oldSelectedItem != newSelectedItem)
{
if (newSelectedItem != null && AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementSelected))
{
AutomationPeer peer = FrameworkElementAutomationPeer.CreatePeerForElement(newSelectedItem);
if (peer != null)
{
peer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementSelected);
}
}
if (oldSelectedItem != null && AutomationPeer.ListenerExists(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection))
{
AutomationPeer peer = FrameworkElementAutomationPeer.CreatePeerForElement(oldSelectedItem);
if (peer != null)
{
peer.RaiseAutomationEvent(AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection);
}
}
}
if (HoveredRatingItem == null)
{
DisplayValue = Value.GetValueOrDefault();
}
}
///
/// Updates the value and actual value of the rating items.
///
private void UpdateDisplayValues()
{
IList ratingItems = GetRatingItems().ToList();
IEnumerable> itemAndWeights =
EnumerableFunctions
.Zip(
ratingItems,
ratingItems
.Select(ratingItem => 1.0)
.GetWeightedValues(DisplayValue),
(item, percent) => Tuple.Create(item, percent));
RatingItem selectedItem = null;
Tuple selectedItemAndWeight = itemAndWeights.LastOrDefault(i => i.Second > 0.0);
if (selectedItemAndWeight != null)
{
selectedItem = selectedItemAndWeight.First;
}
else
{
selectedItem = GetSelectedRatingItem();
}
foreach (Tuple itemAndWeight in itemAndWeights)
{
if (SelectionMode == RatingSelectionMode.Individual && itemAndWeight.First != selectedItem)
{
itemAndWeight.First.DisplayValue = 0.0;
}
else
{
itemAndWeight.First.DisplayValue = itemAndWeight.Second;
}
}
}
///
/// Updates the hover states of the rating items.
///
private void UpdateHoverStates()
{
if (HoveredRatingItem != null && !IsReadOnly)
{
IList ratingItems = GetRatingItems().ToList();
int indexOfItem = ratingItems.IndexOf(HoveredRatingItem);
double total = ratingItems.Count();
double filled = indexOfItem + 1;
this.DisplayValue = filled / total;
for (int cnt = 0; cnt < ratingItems.Count; cnt++)
{
RatingItem ratingItem = ratingItems[cnt];
if (cnt <= indexOfItem && this.SelectionMode == RatingSelectionMode.Continuous)
{
VisualStates.GoToState(ratingItem, true, VisualStates.StateMouseOver);
}
else
{
IUpdateVisualState updateVisualState = (IUpdateVisualState) ratingItem;
updateVisualState.UpdateVisualState(true);
}
}
}
else
{
this.DisplayValue = this.Value.GetValueOrDefault();
foreach (IUpdateVisualState updateVisualState in GetRatingItems().OfType())
{
updateVisualState.UpdateVisualState(true);
}
}
}
///
/// This method returns a container for the item.
///
/// A container for the item.
protected override DependencyObject GetContainerForItemOverride()
{
return new RatingItem();
}
///
/// Gets a value indicating whether the item is its own container.
///
/// The item which may be a container.
/// A value indicating whether the item is its own container.
///
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is RatingItem;
}
///
/// This method prepares a container to host an item.
///
/// The container.
/// The item hosted in the container.
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
RatingItem ratingItem = (RatingItem)element;
object defaultForegroundValue = ratingItem.ReadLocalValue(Control.ForegroundProperty);
if (defaultForegroundValue == DependencyProperty.UnsetValue)
{
ratingItem.SetBinding(Control.ForegroundProperty, new Binding("Foreground") { Source = this });
}
ratingItem.IsReadOnly = this.IsReadOnly;
if (ratingItem.Style == null)
{
ratingItem.Style = this.ItemContainerStyle;
}
ratingItem.Click += RatingItemClick;
ratingItem.MouseEnter += RatingItemMouseEnter;
ratingItem.MouseLeave += RatingItemMouseLeave;
ratingItem.ParentRating = this;
base.PrepareContainerForItemOverride(element, item);
}
///
/// This method clears a container used to host an item.
///
/// The container that hosts the item.
/// The item hosted in the container.
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
RatingItem ratingItem = (RatingItem)element;
ratingItem.Click -= RatingItemClick;
ratingItem.MouseEnter -= RatingItemMouseEnter;
ratingItem.MouseLeave -= RatingItemMouseLeave;
ratingItem.ParentRating = null;
if (ratingItem == HoveredRatingItem)
{
HoveredRatingItem = null;
UpdateDisplayValues();
UpdateHoverStates();
}
base.ClearContainerForItemOverride(element, item);
}
///
/// This method is invoked when a rating item's mouse enter event is
/// invoked.
///
/// The source of the event.
/// Information about the event.
private void RatingItemMouseEnter(object sender, MouseEventArgs e)
{
HoveredRatingItem = (RatingItem) sender;
UpdateHoverStates();
}
///
/// This method is invoked when a rating item's mouse leave event is
/// invoked.
///
/// The source of the event.
/// Information about the event.
private void RatingItemMouseLeave(object sender, MouseEventArgs e)
{
HoveredRatingItem = null;
UpdateDisplayValues();
UpdateHoverStates();
}
///
/// Returns a sequence of rating items.
///
/// A sequence of rating items.
internal IEnumerable GetRatingItems()
{
#if SILVERLIGHT
return
Enumerable
.Range(0, this.Items.Count)
.Select(index => (RatingItem)ItemContainerGenerator.ContainerFromIndex(index))
.Where(ratingItem => ratingItem != null);
#else
// The query above returns null in WPF
// Either way, WPF will already contain the RatingItem objects in the Items collection.
return this.Items.Cast();
#endif
}
///
/// Selects a rating item.
///
/// The selected rating item.
internal void SelectRatingItem(RatingItem selectedRatingItem)
{
if (!this.IsReadOnly)
{
IList ratingItems = GetRatingItems().ToList();
IEnumerable weights = ratingItems.Select(ratingItem => 1.0);
double total = ratingItems.Count();
double percent;
if (total != 0)
{
percent = weights.Take(ratingItems.IndexOf(selectedRatingItem) + 1).Sum() / total;
this.Value = percent;
}
}
}
///
/// This method is raised when a rating item value is selected.
///
/// The source of the event.
/// Information about the event.
private void RatingItemClick(object sender, RoutedEventArgs e)
{
if (!this.IsReadOnly)
{
RatingItem item = (RatingItem)sender;
OnRatingItemValueSelected(item, 1.0);
}
}
///
/// Returns the selected rating item.
///
/// The selected rating item.
private RatingItem GetSelectedRatingItem()
{
return this.GetRatingItems().LastOrDefault(ratingItem => ratingItem.Value > 0.0);
}
///
/// This method is invoked when the rating item value is changed.
///
/// The rating item that has changed.
/// The new value.
protected virtual void OnRatingItemValueSelected(RatingItem ratingItem, double newValue)
{
List ratingItems = GetRatingItems().ToList();
double total = ratingItems.Count();
double value =
(ratingItems
.Take(ratingItems.IndexOf(ratingItem))
.Count() + newValue) / total;
this.Value = value;
}
///
/// Returns a RatingItemAutomationPeer for use by the Silverlight
/// automation infrastructure.
///
/// A RatingItemAutomationPeer object for the RatingItem.
protected override AutomationPeer OnCreateAutomationPeer()
{
return new RatingAutomationPeer(this);
}
///
/// Provides handling for the
/// event when a key
/// is pressed while the control has focus.
///
///
/// A that contains
/// the event data.
///
///
/// is null.
///
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Complexity metric is inflated by the switch statements")]
protected override void OnKeyDown(KeyEventArgs e)
{
if (!Interaction.AllowKeyDown(e))
{
return;
}
base.OnKeyDown(e);
if (e.Handled)
{
return;
}
switch (e.Key)
{
case Key.Left:
{
#if SILVERLIGHT
RatingItem ratingItem = FocusManager.GetFocusedElement() as RatingItem;
#else
RatingItem ratingItem = FocusManager.GetFocusedElement(Application.Current.MainWindow) as RatingItem;
#endif
if (ratingItem != null)
{
ratingItem = GetRatingItemAtOffsetFrom(ratingItem, -1);
}
else
{
ratingItem = GetRatingItems().FirstOrDefault();
}
if (ratingItem != null)
{
if (ratingItem.Focus())
{
e.Handled = true;
}
}
}
break;
case Key.Right:
{
#if SILVERLIGHT
RatingItem ratingItem = FocusManager.GetFocusedElement() as RatingItem;
#else
RatingItem ratingItem = FocusManager.GetFocusedElement(Application.Current.MainWindow) as RatingItem;
#endif
if (ratingItem != null)
{
ratingItem = GetRatingItemAtOffsetFrom(ratingItem, 1);
}
else
{
ratingItem = GetRatingItems().FirstOrDefault();
}
if (ratingItem != null)
{
if (ratingItem.Focus())
{
e.Handled = true;
}
}
}
break;
case Key.Add:
{
if (!this.IsReadOnly)
{
RatingItem ratingItem = GetSelectedRatingItem();
if (ratingItem != null)
{
ratingItem = GetRatingItemAtOffsetFrom(ratingItem, 1);
}
else
{
ratingItem = GetRatingItems().FirstOrDefault();
}
if (ratingItem != null)
{
ratingItem.SelectValue();
e.Handled = true;
}
}
}
break;
case Key.Subtract:
{
if (!this.IsReadOnly)
{
RatingItem ratingItem = GetSelectedRatingItem();
if (ratingItem != null)
{
ratingItem = GetRatingItemAtOffsetFrom(ratingItem, -1);
}
if (ratingItem != null)
{
ratingItem.SelectValue();
e.Handled = true;
}
}
}
break;
}
}
///
/// Gets a rating item at a certain index offset from another
/// rating item.
///
/// The rating item.
/// The rating item at an offset from the
/// index of the rating item.
/// The rating item at the offset.
private RatingItem GetRatingItemAtOffsetFrom(RatingItem ratingItem, int offset)
{
IList ratingItems = GetRatingItems().ToList();
int index = ratingItems.IndexOf(ratingItem);
if (index == -1)
{
return null;
}
index += offset;
if (index >= 0 && index < ratingItems.Count)
{
ratingItem = ratingItems[index];
}
else
{
ratingItem = null;
}
return ratingItem;
}
///
/// Updates the visual state.
///
/// A value indicating whether to use transitions.
void IUpdateVisualState.UpdateVisualState(bool useTransitions)
{
Interaction.UpdateVisualStateBase(useTransitions);
}
}
}