Add layered price lookup and category auto-fill

- PriceLookupService: eBay live data → saved listing history → AI estimate,
  each result labelled by source so the user knows how reliable it is
- Revalue row: new "Check eBay" button fetches suggestion and pre-populates
  the price field; shows source label beneath (or "No suggestion available")
- Category auto-fill: AutoFillCategoryAsync takes the top eBay category
  suggestion and fills the field automatically after photo analysis or AI
  title generation; dropdown stays visible so user can override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Foster
2026-04-15 02:44:12 +01:00
parent da0efc1374
commit f65521b9ab
5 changed files with 340 additions and 69 deletions

View File

@@ -0,0 +1,83 @@
using System.Text.RegularExpressions;
using EbayListingTool.Models;
namespace EbayListingTool.Services;
public record PriceSuggestion(decimal Price, string Source, string Label);
/// <summary>
/// Layered price suggestion: eBay live data → own listing history → AI estimate.
/// Returns the first source that produces a result, labelled so the UI can show
/// where the suggestion came from.
/// </summary>
public class PriceLookupService
{
private readonly EbayPriceResearchService _ebay;
private readonly SavedListingsService _history;
private readonly AiAssistantService _ai;
private static readonly Regex PriceRegex =
new(@"PRICE:\s*(\d+\.?\d*)", RegexOptions.IgnoreCase);
public PriceLookupService(
EbayPriceResearchService ebay,
SavedListingsService history,
AiAssistantService ai)
{
_ebay = ebay;
_history = history;
_ai = ai;
}
public async Task<PriceSuggestion?> GetSuggestionAsync(SavedListing listing)
{
// 1. eBay live listings
try
{
var result = await _ebay.GetLivePricesAsync(listing.Title);
if (result.HasSuggestion)
return new PriceSuggestion(
result.Suggested,
"ebay",
$"eBay suggests £{result.Suggested:F2} (from {result.Count} listings)");
}
catch { /* eBay unavailable — fall through */ }
// 2. Own saved listing history — same category, at least 2 data points
var sameCat = _history.Listings
.Where(l => l.Id != listing.Id
&& !string.IsNullOrWhiteSpace(l.Category)
&& l.Category.Equals(listing.Category, StringComparison.OrdinalIgnoreCase)
&& l.Price > 0)
.Select(l => l.Price)
.ToList();
if (sameCat.Count >= 2)
{
var avg = Math.Round(sameCat.Average(), 2);
return new PriceSuggestion(
avg,
"history",
$"Your avg for {listing.Category}: £{avg:F2} ({sameCat.Count} listings)");
}
// 3. AI estimate
try
{
var response = await _ai.SuggestPriceAsync(listing.Title, listing.ConditionNotes);
var match = PriceRegex.Match(response);
if (match.Success
&& decimal.TryParse(match.Groups[1].Value,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var price)
&& price > 0)
{
return new PriceSuggestion(price, "ai", $"AI estimate: £{price:F2}");
}
}
catch { /* AI unavailable */ }
return null;
}
}

View File

@@ -15,6 +15,7 @@ public partial class MainWindow : MetroWindow
private readonly BulkImportService _bulkService; private readonly BulkImportService _bulkService;
private readonly SavedListingsService _savedService; private readonly SavedListingsService _savedService;
private readonly EbayPriceResearchService _priceService; private readonly EbayPriceResearchService _priceService;
private readonly PriceLookupService _priceLookupService;
public MainWindow() public MainWindow()
{ {
@@ -28,13 +29,14 @@ public partial class MainWindow : MetroWindow
_bulkService = new BulkImportService(); _bulkService = new BulkImportService();
_savedService = new SavedListingsService(); _savedService = new SavedListingsService();
_priceService = new EbayPriceResearchService(_auth); _priceService = new EbayPriceResearchService(_auth);
_priceLookupService = new PriceLookupService(_priceService, _savedService, _aiService);
// Photo Analysis tab — no eBay needed // Photo Analysis tab — no eBay needed
PhotoView.Initialise(_aiService, _savedService, _priceService); PhotoView.Initialise(_aiService, _savedService, _priceService);
PhotoView.UseDetailsRequested += OnUseDetailsRequested; PhotoView.UseDetailsRequested += OnUseDetailsRequested;
// Saved Listings tab // Saved Listings tab
SavedView.Initialise(_savedService); SavedView.Initialise(_savedService, _priceLookupService);
// New Listing + Bulk tabs // New Listing + Bulk tabs
SingleView.Initialise(_listingService, _categoryService, _aiService, _auth); SingleView.Initialise(_listingService, _categoryService, _aiService, _auth);

View File

@@ -205,13 +205,69 @@
<TextBlock x:Name="DetailTitle" Grid.Column="0" <TextBlock x:Name="DetailTitle" Grid.Column="0"
FontSize="17" FontWeight="Bold" TextWrapping="Wrap" FontSize="17" FontWeight="Bold" TextWrapping="Wrap"
Foreground="{DynamicResource MahApps.Brushes.Gray1}"/> Foreground="{DynamicResource MahApps.Brushes.Gray1}"/>
<Border Grid.Column="1"
Background="{DynamicResource MahApps.Brushes.Accent}" <!-- Price display + quick revalue -->
CornerRadius="6" Padding="10,4" Margin="10,0,0,0" <StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Top">
VerticalAlignment="Top">
<!-- Normal price badge + Revalue button -->
<StackPanel x:Name="PriceDisplayRow" Orientation="Horizontal">
<Border Background="{DynamicResource MahApps.Brushes.Accent}"
CornerRadius="6" Padding="10,4">
<TextBlock x:Name="DetailPrice" <TextBlock x:Name="DetailPrice"
FontSize="16" FontWeight="Bold" Foreground="White"/> FontSize="16" FontWeight="Bold" Foreground="White"/>
</Border> </Border>
<Button x:Name="RevalueBtn" Click="RevalueBtn_Click"
Height="28" Padding="8,0" Margin="6,0,0,0"
Style="{DynamicResource MahApps.Styles.Button.Square}"
ToolTip="Quick-change the price">
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial Kind="CurrencyGbp" Width="11" Height="11"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock Text="Revalue" FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<!-- Inline revalue editor (hidden by default) -->
<StackPanel x:Name="RevalueRow" Orientation="Vertical"
Visibility="Collapsed" Margin="0,4,0,0">
<StackPanel Orientation="Horizontal">
<mah:NumericUpDown x:Name="RevaluePrice"
Minimum="0" Maximum="99999"
StringFormat="F2" Interval="0.5"
Width="110" Height="30"/>
<Button x:Name="CheckEbayBtn" Click="CheckEbayBtn_Click"
Height="30" Padding="8,0" Margin="6,0,4,0"
Style="{DynamicResource MahApps.Styles.Button.Square}"
ToolTip="Check eBay for a suggested price">
<StackPanel Orientation="Horizontal">
<iconPacks:PackIconMaterial x:Name="CheckEbayIcon"
Kind="Magnify" Width="11" Height="11"
Margin="0,0,4,0" VerticalAlignment="Center"/>
<TextBlock x:Name="CheckEbayText" Text="Check eBay"
FontSize="11" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Click="RevalueSave_Click"
Style="{DynamicResource MahApps.Styles.Button.Square.Accent}"
Height="30" Padding="10,0" Margin="0,0,4,0"
ToolTip="Save new price">
<iconPacks:PackIconMaterial Kind="Check" Width="13" Height="13"/>
</Button>
<Button Click="RevalueCancel_Click"
Style="{DynamicResource MahApps.Styles.Button.Square}"
Height="30" Padding="8,0"
ToolTip="Cancel">
<iconPacks:PackIconMaterial Kind="Close" Width="11" Height="11"/>
</Button>
</StackPanel>
<TextBlock x:Name="PriceSuggestionLabel"
FontSize="10" Margin="0,5,0,0"
Foreground="{DynamicResource MahApps.Brushes.Gray5}"
Visibility="Collapsed" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</Grid> </Grid>
<!-- Meta row: category · date --> <!-- Meta row: category · date -->
@@ -362,6 +418,9 @@
<!-- Photos --> <!-- Photos -->
<TextBlock Text="PHOTOS" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/> <TextBlock Text="PHOTOS" Style="{StaticResource DetailLabel}" Margin="0,0,0,3"/>
<TextBlock Text="First photo is the listing cover. Use ◀ ▶ to reorder."
FontSize="10" Foreground="{DynamicResource MahApps.Brushes.Gray5}"
Margin="0,0,0,6"/>
<ScrollViewer HorizontalScrollBarVisibility="Auto" <ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled"
Margin="0,0,0,10"> Margin="0,0,0,10">

View File

@@ -13,6 +13,7 @@ namespace EbayListingTool.Views;
public partial class SavedListingsView : UserControl public partial class SavedListingsView : UserControl
{ {
private SavedListingsService? _service; private SavedListingsService? _service;
private PriceLookupService? _priceLookup;
private SavedListing? _selected; private SavedListing? _selected;
// Edit mode working state // Edit mode working state
@@ -33,9 +34,10 @@ public partial class SavedListingsView : UserControl
}; };
} }
public void Initialise(SavedListingsService service) public void Initialise(SavedListingsService service, PriceLookupService? priceLookup = null)
{ {
_service = service; _service = service;
_priceLookup = priceLookup;
RefreshList(); RefreshList();
} }
@@ -255,6 +257,10 @@ public partial class SavedListingsView : UserControl
EmptyDetail.Visibility = Visibility.Collapsed; EmptyDetail.Visibility = Visibility.Collapsed;
DetailPanel.Visibility = Visibility.Visible; DetailPanel.Visibility = Visibility.Visible;
// Reset revalue UI
PriceDisplayRow.Visibility = Visibility.Visible;
RevalueRow.Visibility = Visibility.Collapsed;
DetailTitle.Text = listing.Title; DetailTitle.Text = listing.Title;
DetailPrice.Text = listing.PriceDisplay; DetailPrice.Text = listing.PriceDisplay;
DetailCategory.Text = listing.Category; DetailCategory.Text = listing.Category;
@@ -332,6 +338,77 @@ public partial class SavedListingsView : UserControl
catch { } catch { }
} }
// ---- Quick revalue ----
private void RevalueBtn_Click(object sender, RoutedEventArgs e)
{
if (_selected == null) return;
RevaluePrice.Value = (double)_selected.Price;
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
PriceSuggestionLabel.Text = "";
CheckEbayBtn.IsEnabled = _priceLookup != null;
PriceDisplayRow.Visibility = Visibility.Collapsed;
RevalueRow.Visibility = Visibility.Visible;
RevaluePrice.Focus();
}
private void RevalueSave_Click(object sender, RoutedEventArgs e)
{
if (_selected == null || _service == null) return;
_selected.Price = (decimal)(RevaluePrice.Value ?? 0);
_service.Update(_selected);
DetailPrice.Text = _selected.PriceDisplay;
PriceDisplayRow.Visibility = Visibility.Visible;
RevalueRow.Visibility = Visibility.Collapsed;
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
RefreshList();
}
private void RevalueCancel_Click(object sender, RoutedEventArgs e)
{
PriceDisplayRow.Visibility = Visibility.Visible;
RevalueRow.Visibility = Visibility.Collapsed;
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
}
private async void CheckEbayBtn_Click(object sender, RoutedEventArgs e)
{
if (_selected == null || _priceLookup == null) return;
CheckEbayBtn.IsEnabled = false;
CheckEbayIcon.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.Loading;
CheckEbayText.Text = "Checking…";
PriceSuggestionLabel.Visibility = Visibility.Collapsed;
try
{
var suggestion = await _priceLookup.GetSuggestionAsync(_selected);
if (suggestion != null)
{
RevaluePrice.Value = (double)suggestion.Price;
PriceSuggestionLabel.Text = suggestion.Label;
PriceSuggestionLabel.Visibility = Visibility.Visible;
}
else
{
PriceSuggestionLabel.Text = "No suggestion available — enter price manually.";
PriceSuggestionLabel.Visibility = Visibility.Visible;
}
}
catch (Exception ex)
{
PriceSuggestionLabel.Text = $"Lookup failed: {ex.Message}";
PriceSuggestionLabel.Visibility = Visibility.Visible;
}
finally
{
CheckEbayIcon.Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.Magnify;
CheckEbayText.Text = "Check eBay";
CheckEbayBtn.IsEnabled = true;
}
}
// ---- Edit mode ---- // ---- Edit mode ----
private void EditListing_Click(object sender, RoutedEventArgs e) private void EditListing_Click(object sender, RoutedEventArgs e)
@@ -372,9 +449,17 @@ public partial class SavedListingsView : UserControl
var path = _editPhotoPaths[i]; var path = _editPhotoPaths[i];
var index = i; // capture for lambdas var index = i; // capture for lambdas
var container = new Grid { Width = 120, Height = 120, Margin = new Thickness(0, 0, 8, 0) }; // Outer StackPanel: photo tile on top, reorder buttons below
var outer = new StackPanel
{
Orientation = Orientation.Vertical,
Margin = new Thickness(0, 0, 8, 0),
Width = 120
};
// --- Photo tile (image + cover badge + remove button) ---
var photoGrid = new Grid { Width = 120, Height = 120 };
// Photo image
var imgBorder = new Border var imgBorder = new Border
{ {
Width = 120, Height = 120, Width = 120, Height = 120,
@@ -403,9 +488,9 @@ public partial class SavedListingsView : UserControl
AddPhotoIcon(imgBorder); AddPhotoIcon(imgBorder);
} }
container.Children.Add(imgBorder); photoGrid.Children.Add(imgBorder);
// "Cover" badge on the first photo // "Cover" badge — top-left, only on first photo
if (i == 0) if (i == 0)
{ {
var badge = new Border var badge = new Border
@@ -418,10 +503,10 @@ public partial class SavedListingsView : UserControl
Margin = new Thickness(4, 4, 0, 0) Margin = new Thickness(4, 4, 0, 0)
}; };
badge.Child = new TextBlock { Text = "Cover", FontSize = 9, Foreground = Brushes.White }; badge.Child = new TextBlock { Text = "Cover", FontSize = 9, Foreground = Brushes.White };
container.Children.Add(badge); photoGrid.Children.Add(badge);
} }
// Remove (×) button — top-right corner // Remove (×) button — top-right corner of image
var removeBtn = new Button var removeBtn = new Button
{ {
Content = "×", Content = "×",
@@ -442,31 +527,35 @@ public partial class SavedListingsView : UserControl
_editPhotoPaths.RemoveAt(index); _editPhotoPaths.RemoveAt(index);
BuildEditPhotoStrip(); BuildEditPhotoStrip();
}; };
container.Children.Add(removeBtn); photoGrid.Children.Add(removeBtn);
// Left/right reorder buttons — bottom-centre outer.Children.Add(photoGrid);
// --- Reorder buttons below the photo ---
var reorderPanel = new StackPanel var reorderPanel = new StackPanel
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Bottom, Margin = new Thickness(0, 4, 0, 0)
Margin = new Thickness(0, 0, 0, 4)
}; };
if (i > 0)
{
var leftBtn = new Button var leftBtn = new Button
{ {
Content = "◀", Width = 52, Height = 24,
Width = 26, Height = 20, FontSize = 10,
FontSize = 9,
Padding = new Thickness(0), Padding = new Thickness(0),
Margin = new Thickness(0, 0, 2, 0), Margin = new Thickness(0, 0, 2, 0),
Style = (Style)FindResource("MahApps.Styles.Button.Square"), Style = (Style)FindResource("MahApps.Styles.Button.Square"),
Foreground = Brushes.White, ToolTip = "Move left",
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)), IsEnabled = i > 0
ToolTip = "Move left"
}; };
leftBtn.Content = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
((StackPanel)leftBtn.Content).Children.Add(new MahApps.Metro.IconPacks.PackIconMaterial
{
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronLeft,
Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center
});
((StackPanel)leftBtn.Content).Children.Add(new TextBlock { Text = "Move", FontSize = 9, Margin = new Thickness(2, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center });
leftBtn.Click += (s, e) => leftBtn.Click += (s, e) =>
{ {
(_editPhotoPaths[index], _editPhotoPaths[index - 1]) = (_editPhotoPaths[index], _editPhotoPaths[index - 1]) =
@@ -474,21 +563,23 @@ public partial class SavedListingsView : UserControl
BuildEditPhotoStrip(); BuildEditPhotoStrip();
}; };
reorderPanel.Children.Add(leftBtn); reorderPanel.Children.Add(leftBtn);
}
if (i < _editPhotoPaths.Count - 1)
{
var rightBtn = new Button var rightBtn = new Button
{ {
Content = "▶", Width = 52, Height = 24,
Width = 26, Height = 20, FontSize = 10,
FontSize = 9,
Padding = new Thickness(0), Padding = new Thickness(0),
Style = (Style)FindResource("MahApps.Styles.Button.Square"), Style = (Style)FindResource("MahApps.Styles.Button.Square"),
Foreground = Brushes.White, ToolTip = "Move right",
Background = new SolidColorBrush(Color.FromArgb(180, 40, 40, 40)), IsEnabled = i < _editPhotoPaths.Count - 1
ToolTip = "Move right"
}; };
rightBtn.Content = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
((StackPanel)rightBtn.Content).Children.Add(new TextBlock { Text = "Move", FontSize = 9, Margin = new Thickness(0, 0, 2, 0), VerticalAlignment = VerticalAlignment.Center });
((StackPanel)rightBtn.Content).Children.Add(new MahApps.Metro.IconPacks.PackIconMaterial
{
Kind = MahApps.Metro.IconPacks.PackIconMaterialKind.ChevronRight,
Width = 12, Height = 12, VerticalAlignment = VerticalAlignment.Center
});
rightBtn.Click += (s, e) => rightBtn.Click += (s, e) =>
{ {
(_editPhotoPaths[index], _editPhotoPaths[index + 1]) = (_editPhotoPaths[index], _editPhotoPaths[index + 1]) =
@@ -496,12 +587,9 @@ public partial class SavedListingsView : UserControl
BuildEditPhotoStrip(); BuildEditPhotoStrip();
}; };
reorderPanel.Children.Add(rightBtn); reorderPanel.Children.Add(rightBtn);
}
if (reorderPanel.Children.Count > 0) outer.Children.Add(reorderPanel);
container.Children.Add(reorderPanel); EditPhotosPanel.Children.Add(outer);
EditPhotosPanel.Children.Add(container);
} }
// "Add photos" button at the end of the strip // "Add photos" button at the end of the strip

View File

@@ -18,6 +18,7 @@ public partial class SingleItemView : UserControl
private ListingDraft _draft = new(); private ListingDraft _draft = new();
private System.Threading.CancellationTokenSource? _categoryCts; private System.Threading.CancellationTokenSource? _categoryCts;
private bool _suppressCategoryLookup;
private string _suggestedPriceValue = ""; private string _suggestedPriceValue = "";
// Photo drag-reorder // Photo drag-reorder
@@ -50,7 +51,7 @@ public partial class SingleItemView : UserControl
} }
/// <summary>Pre-fills the form from a Photo Analysis result.</summary> /// <summary>Pre-fills the form from a Photo Analysis result.</summary>
public void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList<string> imagePaths, decimal price) public async void PopulateFromAnalysis(PhotoAnalysisResult result, IReadOnlyList<string> imagePaths, decimal price)
{ {
// Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which // Q6: reset form directly — calling NewListing_Click shows a confirmation dialog which
// is unexpected when arriving here automatically from the Photo Analysis tab. // is unexpected when arriving here automatically from the Photo Analysis tab.
@@ -71,9 +72,9 @@ public partial class SingleItemView : UserControl
TitleBox.Text = result.Title; TitleBox.Text = result.Title;
DescriptionBox.Text = result.Description; DescriptionBox.Text = result.Description;
PriceBox.Value = (double)price; PriceBox.Value = (double)price;
CategoryBox.Text = result.CategoryKeyword;
_draft.CategoryName = result.CategoryKeyword; // Auto-fill the top eBay category from the analysis keyword; user can override
await AutoFillCategoryAsync(result.CategoryKeyword);
// Q1: load all photos from analysis // Q1: load all photos from analysis
var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToArray(); var validPaths = imagePaths.Where(p => !string.IsNullOrEmpty(p) && File.Exists(p)).ToArray();
@@ -117,6 +118,10 @@ public partial class SingleItemView : UserControl
{ {
var title = await _aiService.GenerateTitleAsync(current, condition); var title = await _aiService.GenerateTitleAsync(current, condition);
TitleBox.Text = title.Trim().TrimEnd('.').Trim('"'); TitleBox.Text = title.Trim().TrimEnd('.').Trim('"');
// Auto-fill category from the generated title if not already set
if (string.IsNullOrWhiteSpace(_draft.CategoryId))
await AutoFillCategoryAsync(TitleBox.Text);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -129,6 +134,8 @@ public partial class SingleItemView : UserControl
private async void CategoryBox_TextChanged(object sender, TextChangedEventArgs e) private async void CategoryBox_TextChanged(object sender, TextChangedEventArgs e)
{ {
if (_suppressCategoryLookup) return;
_categoryCts?.Cancel(); _categoryCts?.Cancel();
_categoryCts?.Dispose(); _categoryCts?.Dispose();
_categoryCts = new System.Threading.CancellationTokenSource(); _categoryCts = new System.Threading.CancellationTokenSource();
@@ -211,6 +218,38 @@ public partial class SingleItemView : UserControl
} }
} }
/// <summary>
/// Fetches the top eBay category suggestion for <paramref name="keyword"/> and auto-fills
/// the category fields. The suggestions list is shown so the user can override.
/// </summary>
private async Task AutoFillCategoryAsync(string keyword)
{
if (_categoryService == null || string.IsNullOrWhiteSpace(keyword)) return;
try
{
var suggestions = await _categoryService.GetCategorySuggestionsAsync(keyword);
if (suggestions.Count == 0) return;
var top = suggestions[0];
_suppressCategoryLookup = true;
try
{
_draft.CategoryId = top.CategoryId;
_draft.CategoryName = top.CategoryName;
CategoryBox.Text = top.CategoryName;
CategoryIdLabel.Text = $"ID: {top.CategoryId}";
}
finally { _suppressCategoryLookup = false; }
// Show the full list so user can see alternatives and override
CategorySuggestionsList.ItemsSource = suggestions;
CategorySuggestionsList.Visibility = suggestions.Count > 1
? Visibility.Visible : Visibility.Collapsed;
}
catch { /* non-critical — leave category blank if lookup fails */ }
}
// ---- Condition ---- // ---- Condition ----
private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e) private void ConditionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)