1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-22 02:15:37 +08:00

Merge branch 'master' into fix-song-select-weirdness

This commit is contained in:
Dan Balasescu 2020-02-12 15:01:15 +09:00 committed by GitHub
commit 866b6bb058
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 119 additions and 47 deletions

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
protected override void OnMouseUp(MouseUpEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
EndPlacement(); EndPlacement(true);
base.OnMouseUp(e); base.OnMouseUp(e);
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -30,12 +30,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
EndPlacement(); EndPlacement(true);
return true; return true;
} }
public override void UpdatePosition(Vector2 screenSpacePosition) public override void UpdatePosition(Vector2 screenSpacePosition)
{ {
BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition); HitObject.Position = ToLocalSpace(screenSpacePosition);
} }
} }

View File

@ -68,6 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
switch (state) switch (state)
{ {
case PlacementState.Initial: case PlacementState.Initial:
BeginPlacement();
HitObject.Position = ToLocalSpace(screenSpacePosition); HitObject.Position = ToLocalSpace(screenSpacePosition);
break; break;
@ -132,7 +133,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void endCurve() private void endCurve()
{ {
updateSlider(); updateSlider();
EndPlacement(); EndPlacement(true);
} }
protected override void Update() protected override void Update()

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return false; return false;
HitObject.EndTime = EditorClock.CurrentTime; HitObject.EndTime = EditorClock.CurrentTime;
EndPlacement(); EndPlacement(true);
} }
else else
{ {

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" />
</ItemGroup> </ItemGroup>

View File

@ -446,7 +446,7 @@ namespace osu.Game
/// </summary> /// </summary>
/// <param name="action">The action to perform once we are in the correct state.</param> /// <param name="action">The action to perform once we are in the correct state.</param>
/// <param name="validScreens">An optional collection of valid screen types. If any of these screens are already current we can perform the action immediately, else the first valid parent will be made current before performing the action. <see cref="MainMenu"/> is used if not specified.</param> /// <param name="validScreens">An optional collection of valid screen types. If any of these screens are already current we can perform the action immediately, else the first valid parent will be made current before performing the action. <see cref="MainMenu"/> is used if not specified.</param>
protected void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null) public void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null)
{ {
performFromMainMenuTask?.Cancel(); performFromMainMenuTask?.Cancel();

View File

@ -118,27 +118,43 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer InternalChild = new GridContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, ColumnDimensions = new[]
Spacing = new Vector2(0, 1), {
Children = new[] new Dimension(GridSizeMode.AutoSize, minSize: minWidth ?? 0)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 4),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{ {
text = new OsuSpriteText text = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold),
Text = title.ToUpper() Text = title.ToUpper()
}
}, },
new Drawable[]
{
separator = new Box separator = new Box
{ {
RelativeSizeAxes = minWidth == null ? Axes.X : Axes.None, Anchor = Anchor.CentreLeft,
Width = minWidth ?? 1f, RelativeSizeAxes = Axes.X,
Height = 2, Height = 2
Margin = new MarginPadding { Top = 2 } }
}, },
new[]
{
content content
} }
}
}; };
} }

View File

@ -251,15 +251,22 @@ namespace osu.Game.Rulesets.Edit
public void BeginPlacement(HitObject hitObject) public void BeginPlacement(HitObject hitObject)
{ {
EditorBeatmap.PlacementObject.Value = hitObject;
if (distanceSnapGrid != null) if (distanceSnapGrid != null)
hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time; hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time;
} }
public void EndPlacement(HitObject hitObject) public void EndPlacement(HitObject hitObject, bool commit)
{
EditorBeatmap.PlacementObject.Value = null;
if (commit)
{ {
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
adjustableClock.Seek(hitObject.StartTime); adjustableClock.Seek(hitObject.GetEndTime());
}
showGridFor(Enumerable.Empty<HitObject>()); showGridFor(Enumerable.Empty<HitObject>());
} }

View File

@ -103,11 +103,12 @@ namespace osu.Game.Rulesets.Edit
/// Signals that the placement of <see cref="HitObject"/> has finished. /// Signals that the placement of <see cref="HitObject"/> has finished.
/// This will destroy this <see cref="PlacementBlueprint"/>, and add the <see cref="HitObject"/> to the <see cref="Beatmap"/>. /// This will destroy this <see cref="PlacementBlueprint"/>, and add the <see cref="HitObject"/> to the <see cref="Beatmap"/>.
/// </summary> /// </summary>
protected void EndPlacement() /// <param name="commit">Whether the object should be committed.</param>
public void EndPlacement(bool commit)
{ {
if (!PlacementBegun) if (!PlacementBegun)
BeginPlacement(); BeginPlacement();
placementHandler.EndPlacement(HitObject); placementHandler.EndPlacement(HitObject, commit);
} }
/// <summary> /// <summary>

View File

@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected DragBox DragBox { get; private set; } protected DragBox DragBox { get; private set; }
private Container<SelectionBlueprint> selectionBlueprints; protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
private SelectionHandler selectionHandler; private SelectionHandler selectionHandler;
@ -62,7 +62,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
DragBox = CreateDragBox(select), DragBox = CreateDragBox(select),
selectionHandler, selectionHandler,
selectionBlueprints = CreateSelectionBlueprintContainer(), SelectionBlueprints = CreateSelectionBlueprintContainer(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue) DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
}); });
@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectedHitObjects.ItemsAdded += objects => selectedHitObjects.ItemsAdded += objects =>
{ {
foreach (var o in objects) foreach (var o in objects)
selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select(); SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select();
SelectionChanged?.Invoke(selectedHitObjects); SelectionChanged?.Invoke(selectedHitObjects);
}; };
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectedHitObjects.ItemsRemoved += objects => selectedHitObjects.ItemsRemoved += objects =>
{ {
foreach (var o in objects) foreach (var o in objects)
selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect(); SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
SelectionChanged?.Invoke(selectedHitObjects); SelectionChanged?.Invoke(selectedHitObjects);
}; };
@ -230,7 +230,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void removeBlueprintFor(HitObject hitObject) private void removeBlueprintFor(HitObject hitObject)
{ {
var blueprint = selectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); var blueprint = SelectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject);
if (blueprint == null) if (blueprint == null)
return; return;
@ -239,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected -= onBlueprintSelected; blueprint.Selected -= onBlueprintSelected;
blueprint.Deselected -= onBlueprintDeselected; blueprint.Deselected -= onBlueprintDeselected;
selectionBlueprints.Remove(blueprint); SelectionBlueprints.Remove(blueprint);
} }
protected virtual void AddBlueprintFor(HitObject hitObject) protected virtual void AddBlueprintFor(HitObject hitObject)
@ -251,7 +251,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected += onBlueprintSelected; blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected; blueprint.Deselected += onBlueprintDeselected;
selectionBlueprints.Add(blueprint); SelectionBlueprints.Add(blueprint);
} }
#endregion #endregion
@ -278,7 +278,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
return; return;
foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveChildren) foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
{ {
if (blueprint.IsHovered) if (blueprint.IsHovered)
{ {
@ -308,7 +308,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param> /// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
private void select(RectangleF rect) private void select(RectangleF rect)
{ {
foreach (var blueprint in selectionBlueprints) foreach (var blueprint in SelectionBlueprints)
{ {
if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint)) if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
blueprint.Select(); blueprint.Select();
@ -322,7 +322,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
private void selectAll() private void selectAll()
{ {
selectionBlueprints.ToList().ForEach(m => m.Select()); SelectionBlueprints.ToList().ForEach(m => m.Select());
selectionHandler.UpdateVisibility(); selectionHandler.UpdateVisibility();
} }
@ -334,14 +334,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void onBlueprintSelected(SelectionBlueprint blueprint) private void onBlueprintSelected(SelectionBlueprint blueprint)
{ {
selectionHandler.HandleSelected(blueprint); selectionHandler.HandleSelected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 1); SelectionBlueprints.ChangeChildDepth(blueprint, 1);
beatmap.SelectedHitObjects.Add(blueprint.HitObject); beatmap.SelectedHitObjects.Add(blueprint.HitObject);
} }
private void onBlueprintDeselected(SelectionBlueprint blueprint) private void onBlueprintDeselected(SelectionBlueprint blueprint)
{ {
selectionHandler.HandleDeselected(blueprint); selectionHandler.HandleDeselected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 0); SelectionBlueprints.ChangeChildDepth(blueprint, 0);
beatmap.SelectedHitObjects.Remove(blueprint.HitObject); beatmap.SelectedHitObjects.Remove(blueprint.HitObject);
} }

View File

@ -63,6 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void refreshTool() private void refreshTool()
{ {
placementBlueprintContainer.Clear(); placementBlueprintContainer.Clear();
currentPlacement?.EndPlacement(false);
currentPlacement = null; currentPlacement = null;
var blueprint = CurrentTool?.CreatePlacementBlueprint(); var blueprint = CurrentTool?.CreatePlacementBlueprint();

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -21,8 +22,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
private DragEvent lastDragEvent; private DragEvent lastDragEvent;
private Bindable<HitObject> placement;
private SelectionBlueprint placementBlueprint;
public TimelineBlueprintContainer() public TimelineBlueprintContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -43,6 +51,29 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
base.LoadComplete(); base.LoadComplete();
DragBox.Alpha = 0; DragBox.Alpha = 0;
placement = beatmap.PlacementObject.GetBoundCopy();
placement.ValueChanged += placementChanged;
}
private void placementChanged(ValueChangedEvent<HitObject> obj)
{
if (obj.NewValue == null)
{
if (placementBlueprint != null)
{
SelectionBlueprints.Remove(placementBlueprint);
placementBlueprint = null;
}
}
else
{
placementBlueprint = CreateBlueprintFor(obj.NewValue);
placementBlueprint.Colour = Color4.MediumPurple;
SelectionBlueprints.Add(placementBlueprint);
}
} }
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };

View File

@ -17,7 +17,8 @@ namespace osu.Game.Screens.Edit.Compose
/// Notifies that a placement has finished. /// Notifies that a placement has finished.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that has been placed.</param> /// <param name="hitObject">The <see cref="HitObject"/> that has been placed.</param>
void EndPlacement(HitObject hitObject); /// <param name="commit">Whether the object should be committed.</param>
void EndPlacement(HitObject hitObject, bool commit);
/// <summary> /// <summary>
/// Deletes a <see cref="HitObject"/>. /// Deletes a <see cref="HitObject"/>.

View File

@ -33,7 +33,15 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public event Action<HitObject> StartTimeChanged; public event Action<HitObject> StartTimeChanged;
public BindableList<HitObject> SelectedHitObjects { get; } = new BindableList<HitObject>(); /// <summary>
/// All currently selected <see cref="HitObject"/>s.
/// </summary>
public readonly BindableList<HitObject> SelectedHitObjects = new BindableList<HitObject>();
/// <summary>
/// The current placement. Null if there's no active placement.
/// </summary>
public readonly Bindable<HitObject> PlacementObject = new Bindable<HitObject>();
public readonly IBeatmap PlayableBeatmap; public readonly IBeatmap PlayableBeatmap;

View File

@ -141,12 +141,15 @@ namespace osu.Game.Screens.Menu
preloadSongSelect(); preloadSongSelect();
} }
[Resolved]
private OsuGame game { get; set; }
private void confirmAndExit() private void confirmAndExit()
{ {
if (exitConfirmed) return; if (exitConfirmed) return;
exitConfirmed = true; exitConfirmed = true;
this.Exit(); game.PerformFromScreen(menu => menu.Exit());
} }
private void preloadSongSelect() private void preloadSongSelect()

View File

@ -53,8 +53,9 @@ namespace osu.Game.Tests.Visual
{ {
} }
public void EndPlacement(HitObject hitObject) public void EndPlacement(HitObject hitObject, bool commit)
{ {
if (commit)
AddHitObject(CreateHitObject(hitObject)); AddHitObject(CreateHitObject(hitObject));
Remove(currentBlueprint); Remove(currentBlueprint);

View File

@ -24,7 +24,7 @@
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.207.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.207.0" />
<PackageReference Include="Sentry" Version="2.0.1" /> <PackageReference Include="Sentry" Version="2.0.2" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />