diff --git a/maui/src/Calendar/Views/CalendarView/MonthView/MonthView.cs b/maui/src/Calendar/Views/CalendarView/MonthView/MonthView.cs index 1649f2ad..9d2a2387 100644 --- a/maui/src/Calendar/Views/CalendarView/MonthView/MonthView.cs +++ b/maui/src/Calendar/Views/CalendarView/MonthView/MonthView.cs @@ -880,6 +880,24 @@ internal void SetFocusOnViewChanged() #region Private Methods + /// + /// Finds the special date icon details for a given date using a loop to avoid delegate allocation. + /// + /// The date to look up. + /// The matching CalendarIconDetails or null. + CalendarIconDetails? GetSpecialDateIcon(DateTime dateTime) + { + for (int i = 0; i < _specialDates.Count; i++) + { + if (CalendarViewHelper.IsSameDate(_calendarViewInfo.View, _specialDates[i].Date, dateTime, _calendarViewInfo.Identifier)) + { + return _specialDates[i]; + } + } + + return null; + } + /// /// Method to find the range is present in current view or not. /// @@ -1635,7 +1653,7 @@ void DrawMonthCells(ICanvas canvas, bool isRTL, float weekNumberWidth, float mon // The current date is today date and not a range then need to considered the today text style. bool isTodayDate = todayDate.Date.Equals(dateTime.Date); //// Stores the special dates icon details for drawing. - CalendarIconDetails? calendarSpecialDayIconDetails = _specialDates.FirstOrDefault(details => CalendarViewHelper.IsSameDate(_calendarViewInfo.View, details.Date, dateTime, _calendarViewInfo.Identifier)); + CalendarIconDetails? calendarSpecialDayIconDetails = GetSpecialDateIcon(dateTime); CalendarTextStyle textStyle = GetMonthCellStyle(dateTime, isTodayDate, isLeadingAndTrailingDates, isBlackoutDate, isDisabledDate, _calendarViewInfo.ShowOutOfRangeDates, calendarSpecialDayIconDetails != null, ref fillColor, cellBackground, trailingLeadingDateBackground, weekendsBackground, todayBackground, disabledDatesBackground, specialDatesBackground, cultureCalendar); //// If background color is not transparent then the background color for month cell is applied. if (fillColor != Colors.Transparent) @@ -3080,7 +3098,7 @@ private void OnDateSemanticsNodeClicked(SemanticsNode node) string blackOutDate = CalendarViewHelper.IsDateInDateCollection(dateTime, _disabledDates) ? SfCalendarResources.GetLocalizedString("Blackout Date") : string.Empty; string disabledDate = CalendarViewHelper.IsDisabledDate(dateTime, _calendarViewInfo.View, _calendarViewInfo.EnablePastDates, _calendarViewInfo.MinimumDate, _calendarViewInfo.MaximumDate, _calendarViewInfo.SelectionMode, _calendarViewInfo.RangeSelectionDirection, _selectedRange, _calendarViewInfo.AllowViewNavigation, _calendarViewInfo.Identifier) ? SfCalendarResources.GetLocalizedString("Disabled Date") : string.Empty; - CalendarIconDetails? calendarSpecialDayIconDetails = _specialDates.FirstOrDefault(details => CalendarViewHelper.IsSameDate(_calendarViewInfo.View, details.Date, dateTime, _calendarViewInfo.Identifier)); + CalendarIconDetails? calendarSpecialDayIconDetails = GetSpecialDateIcon(dateTime); string specialDate = calendarSpecialDayIconDetails == null ? string.Empty : SfCalendarResources.GetLocalizedString("Special Date"); string dateType = string.IsNullOrEmpty(specialDate) ? !string.IsNullOrEmpty(blackOutDate) ? blackOutDate : disabledDate : specialDate; string dateText = isGregorianCalendar ? dateTime.ToString("dddd, dd/MMMM/yyyy") + dateType : dateTime.ToString("dddd, dd/MMMM/yyyy", cultureInfo) + dateType; diff --git a/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs b/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs index 71cfc5df..c4452b84 100644 --- a/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs +++ b/maui/src/Charts/Axis/Layouts/CartesianAxisLayout.cs @@ -499,10 +499,17 @@ void InternalCreateSegments(ChartSeries series) static ChartAxis? GetAxisByName(string name, ObservableCollection? axes) { - var item = (from x in axes where x.Name == name select x).ToList(); - if (item != null && item.Count > 0) + if (axes == null) + { + return null; + } + + foreach (var axis in axes) { - return item[0]; + if (axis.Name == name) + { + return axis; + } } return null; diff --git a/maui/src/Charts/Segment/PolarAreaSegment.cs b/maui/src/Charts/Segment/PolarAreaSegment.cs index e5d0c130..9db27245 100644 --- a/maui/src/Charts/Segment/PolarAreaSegment.cs +++ b/maui/src/Charts/Segment/PolarAreaSegment.cs @@ -212,13 +212,14 @@ void DrawPath(ICanvas canvas, List? fillPoints, List? strokePoints List GenerateInteriorPoints(float animationValue) { - var fillPoints = new List(); - if (Series is not PolarSeries series) { - return fillPoints; + return new List(); } + int capacity = (_pointsCount + 2) * 2; + var fillPoints = new List(capacity); + if (series.ActualXAxis != null && series.ActualYAxis != null && _xValues != null && _yValues != null) { PointF pointF = series.TransformVisiblePoint(_xValues[0], _yValues[0], animationValue); @@ -252,13 +253,14 @@ List GenerateInteriorPoints(float animationValue) List GenerateStrokePoints(float animationValue) { - var strokePoints = new List(); - if (Series is not PolarSeries series) { - return strokePoints; + return new List(); } + int capacity = (_pointsCount + 2) * 2; + var strokePoints = new List(capacity); + if (series.ActualXAxis != null && series.ActualYAxis != null && _xValues != null && _yValues != null) { PointF startPoint = series.TransformVisiblePoint(_xValues[0], _yValues[0], animationValue); diff --git a/maui/src/Charts/Series/ChartSeries.cs b/maui/src/Charts/Series/ChartSeries.cs index 62d44d92..8ead0fa9 100644 --- a/maui/src/Charts/Series/ChartSeries.cs +++ b/maui/src/Charts/Series/ChartSeries.cs @@ -1582,6 +1582,7 @@ internal void DataLabelSettings_PropertyChanged(object? sender, System.Component if (e.PropertyName == nameof(dataLabelSettings.LabelStyle)) { + dataLabelSettings.LabelStyle.PropertyChanged -= LabelStyle_PropertyChanged; dataLabelSettings.LabelStyle.PropertyChanged += LabelStyle_PropertyChanged; dataLabelSettings.LabelStyle.Parent = Parent; } diff --git a/maui/src/Picker/SfTimePicker.cs b/maui/src/Picker/SfTimePicker.cs index 39532836..df168151 100644 --- a/maui/src/Picker/SfTimePicker.cs +++ b/maui/src/Picker/SfTimePicker.cs @@ -893,6 +893,23 @@ internal void UpdateFormat() #region Private Methods + /// + /// Checks if the collection contains a string that parses to the specified integer value, + /// avoiding LINQ Select/Contains allocation overhead. + /// + static bool ContainsParsedValue(ObservableCollection collection, int value) + { + for (int i = 0; i < collection.Count; i++) + { + if (int.TryParse(collection[i], out int parsed) && parsed == value) + { + return true; + } + } + + return false; + } + /// /// Method trigged whenever the base panel selection is changed. /// @@ -1002,7 +1019,7 @@ void UpdateHourColumn(PickerSelectionChangedEventArgs e, string hourFormat, Time } int minute = 0; - if (_minuteColumn.ItemsSource != null && _minuteColumn.ItemsSource is ObservableCollection minuteCollection && minuteCollection.Select(m => int.Parse(m)).Contains(previousSelectedTime.Value.Minutes)) + if (_minuteColumn.ItemsSource != null && _minuteColumn.ItemsSource is ObservableCollection minuteCollection && ContainsParsedValue(minuteCollection, previousSelectedTime.Value.Minutes)) { minute = previousSelectedTime.Value.Minutes; } @@ -1015,7 +1032,7 @@ void UpdateHourColumn(PickerSelectionChangedEventArgs e, string hourFormat, Time } int second = 0; - if (_secondColumn.ItemsSource != null && _secondColumn.ItemsSource is ObservableCollection secondCollection && secondCollection.Select(m => int.Parse(m)).Contains(previousSelectedTime.Value.Seconds)) + if (_secondColumn.ItemsSource != null && _secondColumn.ItemsSource is ObservableCollection secondCollection && ContainsParsedValue(secondCollection, previousSelectedTime.Value.Seconds)) { second = previousSelectedTime.Value.Seconds; } @@ -1064,7 +1081,7 @@ void UpdateMinuteColumn(PickerSelectionChangedEventArgs e, TimeSpan? previousSel } int second = 0; - if (_secondColumn.ItemsSource != null && _secondColumn.ItemsSource is ObservableCollection secondCollection && secondCollection.Select(m => int.Parse(m)).Contains(previousSelectedTime.Value.Seconds)) + if (_secondColumn.ItemsSource != null && _secondColumn.ItemsSource is ObservableCollection secondCollection && ContainsParsedValue(secondCollection, previousSelectedTime.Value.Seconds)) { second = previousSelectedTime.Value.Seconds; } diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/PerformanceOptimizationTests.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/PerformanceOptimizationTests.cs new file mode 100644 index 00000000..4d99b23e --- /dev/null +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Chart/PerformanceOptimizationTests.cs @@ -0,0 +1,161 @@ +using Syncfusion.Maui.Toolkit.Charts; + +namespace Syncfusion.Maui.Toolkit.UnitTest.Charts +{ + public class PerformanceOptimizationTests + { + #region CartesianAxisLayout - GetAxisByName + + [Fact] + public void CartesianChart_NamedAxes_AssociateCorrectly() + { + var chart = new SfCartesianChart(); + var xAxis = new NumericalAxis { Name = "xAxis1" }; + var yAxis = new NumericalAxis { Name = "yAxis1" }; + + chart.XAxes.Add(xAxis); + chart.YAxes.Add(yAxis); + + var series = new LineSeries + { + XAxisName = "xAxis1", + YAxisName = "yAxis1" + }; + chart.Series.Add(series); + + Assert.Equal("xAxis1", xAxis.Name); + Assert.Equal("yAxis1", yAxis.Name); + Assert.Equal("xAxis1", series.XAxisName); + Assert.Equal("yAxis1", series.YAxisName); + } + + [Fact] + public void CartesianChart_MultipleNamedAxes_FindsCorrectAxis() + { + var chart = new SfCartesianChart(); + var xAxis1 = new NumericalAxis { Name = "primary" }; + var xAxis2 = new NumericalAxis { Name = "secondary" }; + var yAxis1 = new NumericalAxis { Name = "left" }; + var yAxis2 = new NumericalAxis { Name = "right" }; + + chart.XAxes.Add(xAxis1); + chart.XAxes.Add(xAxis2); + chart.YAxes.Add(yAxis1); + chart.YAxes.Add(yAxis2); + + var series1 = new LineSeries + { + XAxisName = "primary", + YAxisName = "left" + }; + var series2 = new LineSeries + { + XAxisName = "secondary", + YAxisName = "right" + }; + + chart.Series.Add(series1); + chart.Series.Add(series2); + + Assert.Equal(2, chart.XAxes.Count); + Assert.Equal(2, chart.YAxes.Count); + Assert.Equal("secondary", series2.XAxisName); + Assert.Equal("right", series2.YAxisName); + } + + [Fact] + public void CartesianChart_NullAxisName_DoesNotThrow() + { + var chart = new SfCartesianChart(); + var xAxis = new NumericalAxis(); + var yAxis = new NumericalAxis(); + + chart.XAxes.Add(xAxis); + chart.YAxes.Add(yAxis); + + var series = new LineSeries(); + chart.Series.Add(series); + + Assert.Null(series.XAxisName); + Assert.Null(series.YAxisName); + } + + #endregion + + #region DataLabelSettings - LabelStyle PropertyChanged handler + + [Fact] + public void DataLabelSettings_LabelStyle_CanBeReassigned() + { + var settings = new CartesianDataLabelSettings(); + var labelStyle1 = new ChartDataLabelStyle { TextColor = Colors.Red }; + var labelStyle2 = new ChartDataLabelStyle { TextColor = Colors.Blue }; + + settings.LabelStyle = labelStyle1; + Assert.Equal(Colors.Red, settings.LabelStyle.TextColor); + + settings.LabelStyle = labelStyle2; + Assert.Equal(Colors.Blue, settings.LabelStyle.TextColor); + } + + [Fact] + public void DataLabelSettings_LabelStyle_ReassignmentDoesNotThrow() + { + var series = new ColumnSeries(); + var settings = new CartesianDataLabelSettings(); + series.DataLabelSettings = settings; + + var exception = Record.Exception(() => + { + for (int i = 0; i < 10; i++) + { + settings.LabelStyle = new ChartDataLabelStyle + { + TextColor = Colors.Red, + FontSize = 12 + i + }; + } + }); + + Assert.Null(exception); + } + + #endregion + + #region PolarAreaSegment - List capacity optimization + + [Fact] + public void PolarAreaSeries_AddedToChart_DoesNotThrow() + { + var chart = new SfPolarChart(); + var primaryAxis = new NumericalAxis(); + var secondaryAxis = new NumericalAxis(); + chart.PrimaryAxis = primaryAxis; + chart.SecondaryAxis = secondaryAxis; + + var series = new PolarAreaSeries(); + chart.Series.Add(series); + + Assert.Single(chart.Series); + Assert.IsType(chart.Series[0]); + } + + [Fact] + public void PolarAreaSeries_WithData_InitializesCorrectly() + { + var chart = new SfPolarChart(); + chart.PrimaryAxis = new NumericalAxis(); + chart.SecondaryAxis = new NumericalAxis(); + + var series = new PolarAreaSeries + { + IsClosed = true + }; + chart.Series.Add(series); + + Assert.True(series.IsClosed); + } + + #endregion + } +} diff --git a/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Picker/TimePickerPerformanceTests.cs b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Picker/TimePickerPerformanceTests.cs new file mode 100644 index 00000000..3270cfbc --- /dev/null +++ b/maui/tests/Syncfusion.Maui.Toolkit.UnitTest/Picker/TimePickerPerformanceTests.cs @@ -0,0 +1,87 @@ +using Syncfusion.Maui.Toolkit.Picker; + +namespace Syncfusion.Maui.Toolkit.UnitTest +{ + public class TimePickerPerformanceTests : PickerBaseUnitTest + { + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(15)] + public void TimePicker_MinuteInterval_SetAndGet(int interval) + { + var picker = new SfTimePicker + { + MinuteInterval = interval + }; + + Assert.Equal(interval, picker.MinuteInterval); + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + [InlineData(15)] + public void TimePicker_SecondInterval_SetAndGet(int interval) + { + var picker = new SfTimePicker + { + SecondInterval = interval + }; + + Assert.Equal(interval, picker.SecondInterval); + } + + [Fact] + public void TimePicker_SelectedTimePreserved_WhenIntervalsChange() + { + var picker = new SfTimePicker + { + SelectedTime = new TimeSpan(10, 30, 45) + }; + + Assert.Equal(new TimeSpan(10, 30, 45), picker.SelectedTime); + + picker.MinuteInterval = 5; + + // The selected time should still be accessible + Assert.NotNull(picker.SelectedTime); + } + + [Theory] + [InlineData("10:30:00")] + [InlineData("23:59:59")] + [InlineData("00:00:00")] + public void TimePicker_SelectedTime_SetAndRetrieve(string timeStr) + { + var expected = TimeSpan.Parse(timeStr); + var picker = new SfTimePicker + { + SelectedTime = expected + }; + + Assert.Equal(expected, picker.SelectedTime); + } + + [Fact] + public void TimePicker_MultipleIntervalChanges_DoNotThrow() + { + var picker = new SfTimePicker + { + SelectedTime = new TimeSpan(12, 15, 30) + }; + + var exception = Record.Exception(() => + { + picker.MinuteInterval = 5; + picker.SecondInterval = 10; + picker.MinuteInterval = 15; + picker.SecondInterval = 30; + picker.MinuteInterval = 1; + picker.SecondInterval = 1; + }); + + Assert.Null(exception); + } + } +}