// (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); } } }