Install-Package Microsoft.Extensions.DependencyInjection;
Install-Package CommunityToolkit.mvvm;
<Window x:Class="WpfApp25.Views.MainWin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp25.Views"
WindowState="Maximized"
mc:Ignorable="d"
Title="{Binding MainTitle}" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentControl Grid.Row="0"
Content="{Binding DgViewInstance}"
x:Name="MainRegion"/>
</Grid>
</Window>
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using WpfApp25.Views;
namespace WpfApp25.ViewModels
{
public partial class MainWinViewModel:ObservableObject
{
public MainWinViewModel(DgViewModel dgVM)
{
MainTitle = $"In Main Win,now is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}";
InitTimer();
DgViewInstance = new DgView(dgVM);
}
private void InitTimer()
{
System.Timers.Timer tmr = new System.Timers.Timer();
tmr.Interval = 1000;
tmr.Elapsed += Tmr_Elapsed;
tmr.Start();
}
private void Tmr_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
MainTitle = $"In Main Win,now is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}";
}
[ObservableProperty]
private string mainTitle;
[ObservableProperty]
private string statusMsg;
[ObservableProperty]
private object dgViewInstance;
}
}
//App.xaml
<Application x:Class="WpfApp25.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp25">
<Application.Resources>
</Application.Resources>
</Application>
//App.xaml.cs
using Microsoft.Extensions.DependencyInjection;
using System.Configuration;
using System.Data;
using System.Windows;
using System.Windows.Controls;
using WpfApp25.Services;
using WpfApp25.ViewModels;
using WpfApp25.Views;
namespace WpfApp25
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
ServiceProvider serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
ServiceCollection services = new ServiceCollection();
ConfigureServices(services);
serviceProvider = services.BuildServiceProvider();
var mainWin = serviceProvider.GetRequiredService<MainWin>();
mainWin?.Show();
}
private static void ConfigureServices(ServiceCollection services)
{
services.AddSingleton<IIDService, IDService>();
services.AddSingleton<IISBNService, ISBNService>();
services.AddSingleton<INameService, NameService>();
services.AddSingleton<DgView>();
services.AddSingleton<DgViewModel>();
services.AddSingleton<MainWin>();
services.AddSingleton<MainWinViewModel>();
}
protected override void OnExit(ExitEventArgs e)
{
base.OnExit(e);
serviceProvider?.Dispose();
serviceProvider = null;
}
}
}
//MainWindow.xaml
<Window x:Class="WpfApp25.Views.MainWin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp25.Views"
WindowState="Maximized"
mc:Ignorable="d"
Title="{Binding MainTitle}" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentControl Grid.Row="0"
Content="{Binding DgViewInstance}"
x:Name="MainRegion"/>
</Grid>
</Window>
//MainWin.xaml.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using WpfApp25.ViewModels;
namespace WpfApp25.Views
{
/// <summary>
/// Interaction logic for MainWin.xaml
/// </summary>
public partial class MainWin : Window
{
public MainWin(MainWinViewModel vm)
{
InitializeComponent();
this.DataContext = vm;
}
}
}
//MainWinViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using WpfApp25.Views;
namespace WpfApp25.ViewModels
{
public partial class MainWinViewModel:ObservableObject
{
public MainWinViewModel(DgViewModel dgVM)
{
MainTitle = $"In Main Win,now is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}";
InitTimer();
DgViewInstance = new DgView(dgVM);
}
private void InitTimer()
{
System.Timers.Timer tmr = new System.Timers.Timer();
tmr.Interval = 1000;
tmr.Elapsed += Tmr_Elapsed;
tmr.Start();
}
private void Tmr_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
MainTitle = $"In Main Win,now is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}";
}
[ObservableProperty]
private string mainTitle;
[ObservableProperty]
private string statusMsg;
[ObservableProperty]
private object dgViewInstance;
}
}
//DgView.xaml
<UserControl x:Class="WpfApp25.Views.DgView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp25.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid Grid.Row="0"
ItemsSource="{Binding BooksCollection}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.CacheLengthUnit="Item"
VirtualizingPanel.CacheLength="3,3"
ScrollViewer.IsDeferredScrollingEnabled="True"
ScrollViewer.CanContentScroll="True"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Resources>
<Style TargetType="DataGridRow">
<Setter Property="FontSize" Value="30"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="FontSize" Value="50"/>
<Setter Property="Foreground" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<!--Author,Name,Comment,Content,ISBN,Title,Topic-->
<DataGridTextColumn Binding="{Binding Id}"/>
<DataGridTextColumn Binding="{Binding Author}"/>
<DataGridTextColumn Binding="{Binding Name}"/>
<DataGridTextColumn Binding="{Binding Comment}"/>
<DataGridTextColumn Binding="{Binding Content}"/>
<DataGridTextColumn Binding="{Binding ISBN}"/>
<DataGridTextColumn Binding="{Binding Title}"/>
<DataGridTextColumn Binding="{Binding Topic}"/>
</DataGrid.Columns>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Header="Export In Excel"
Command="{Binding ExportInExcelCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu},Path=PlacementTarget}"/>
</ContextMenu>
</DataGrid.ContextMenu>
</DataGrid>
<TextBlock Grid.Row="1"
Text="{Binding LoadingMsg}"
FontSize="30"/>
</Grid>
</UserControl>
//Dgview.xaml.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using WpfApp25.ViewModels;
namespace WpfApp25.Views
{
/// <summary>
/// Interaction logic for DgView.xaml
/// </summary>
public partial class DgView : UserControl
{
public DgView(DgViewModel vm)
{
InitializeComponent();
this.DataContext = vm;
this.Loaded += async (s, e) =>
{
await vm.InitBooksCollectionAsync();
};
}
}
}
//DgViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Win32;
using OfficeOpenXml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Windows.Controls;
using System.Windows.Input;
using WpfApp25.Models;
using WpfApp25.Services;
using WpfApp25.Utitlity;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Reflection;
using System.Windows;
using System.Linq;
using System.Linq.Expressions;
using Expression = System.Linq.Expressions.Expression;
namespace WpfApp25.ViewModels
{
public partial class DgViewModel : ObservableObject
{
IIDService idService;
INameService nameService;
IISBNService isbnService;
public DgViewModel(IIDService idServiceValue, INameService nameServiceValue, IISBNService isbnServiceValue)
{
idService = idServiceValue;
nameService = nameServiceValue;
isbnService = isbnServiceValue;
ExportInExcelCommand = new DelCommand(ExportInExcelCommandExecuted);
}
int globalProcessedCount = 0;
private void ExportInExcelCommandExecuted(object? obj)
{
var dg = obj as DataGrid;
if (dg != null)
{
Task.Run(() =>
{
var itemsList = dg.Items.Cast<Book>()?.ToList();
if (itemsList == null || !itemsList.Any())
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show("No data to export!");
});
return;
}
string fileName = null;
Application.Current.Dispatcher.Invoke(() =>
{
SaveFileDialog dialog = new SaveFileDialog();
dialog.Filter = "Excel Files|*.xlsx;*.xls";
dialog.FileName = $"Excel_{DateTime.Now:yyyyMMddHHmmss}";
if (dialog.ShowDialog() == true)
{
fileName = dialog.FileName;
}
});
if (!string.IsNullOrEmpty(fileName))
{
var progress = new Progress<int>(percent =>
{
Application.Current.Dispatcher.Invoke(() =>
{
LoadingMsg = $"Exporting... {percent}%";
});
});
try
{
ExportListT(itemsList, fileName, progress);
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show($"Data saved successfully in {fileName}!");
LoadingMsg = "Export completed!";
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
MessageBox.Show($"Export failed: {ex.Message}");
LoadingMsg = "Export failed!";
});
}
}
});
}
}
private void ExportListT<T>(List<T> dataList, string excelFileName, IProgress<int> progress = null)
{
EPPlusLicense license = new EPPlusLicense();
license.SetNonCommercialPersonal("Grit");
using (var package = new ExcelPackage())
{
if (dataList == null || !dataList.Any())
{
var workSheet = package.Workbook.Worksheets.Add("Sheet_1");
workSheet.Cells["A1"].Value = "No data available";
package.SaveAs(new System.IO.FileInfo(excelFileName));
return;
}
const int excelMaxRows = 1048576;
// Reserve 1 row for header
const int maxDataRows = excelMaxRows - 1;
int totalCount = dataList.Count;
int processedCount = 0;
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
if (dataList.Count <= maxDataRows)
{
ExportToWorkSheet_Fast(package, "sheet_1", dataList, progress, totalCount);
}
else
{
int sheetcount = (int)Math.Ceiling((double)dataList.Count / maxDataRows);
for (int sheetIdx = 0; sheetIdx < sheetcount; sheetIdx++)
{
var sheetDataList = dataList.Skip(sheetIdx * maxDataRows)
.Take(maxDataRows)
.ToList();
ExportToWorkSheet_Fast(package, $"Sheet_{sheetIdx + 1}", sheetDataList, progress, totalCount);
}
}
package.SaveAs(new System.IO.FileInfo(excelFileName));
}
}
private void ExportToWorkSheet<T>(ExcelPackage package, string sheetName, List<T> dataList,
IProgress<int> progress = null, int totalCount = 0)
{
var workSheet = package.Workbook.Worksheets.Add(sheetName);
//Header
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
for (int i = 0; i < props.Length; i++)
{
var cell = workSheet.Cells[1, i + 1];
cell.Value = props[i].Name;
cell.Style.Font.Bold = true;
cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
cell.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightBlue);
cell.Style.Border.Bottom.Style = ExcelBorderStyle.Thick;
}
//Data
for (int row = 0; row < dataList.Count; row++)
{
++globalProcessedCount;
var item = dataList[row];
for (int col = 0; col < props.Length; col++)
{
var value = props[col].GetValue(item);
workSheet.Cells[row + 2, col + 1].Value = value;
if (progress != null && (globalProcessedCount % 10000 == 0 || row == dataList.Count - 1))
{
int percent = (int)((double)globalProcessedCount / totalCount * 100);
progress.Report(Math.Min(percent, 100));
LoadingMsg = $"Export processed {percent}%100";
}
}
}
workSheet.Cells[workSheet.Dimension.Address].AutoFitColumns();
}
private void ExportToWorkSheet_Fast<T>(ExcelPackage package, string sheetName, List<T> dataList,
IProgress<int> progress = null, int totalCount = 0)
{
var workSheet = package.Workbook.Worksheets.Add(sheetName);
// Properties and header
var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
for (int i = 0; i < props.Length; i++)
{
var cell = workSheet.Cells[1, i + 1];
cell.Value = props[i].Name;
cell.Style.Font.Bold = true;
cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
cell.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightBlue);
cell.Style.Border.Bottom.Style = ExcelBorderStyle.Thick;
}
if (dataList == null || dataList.Count == 0)
{
package.SaveAs(new System.IO.FileInfo("empty.xlsx"));
return;
}
// Build fast delegates for property getters (cached)
var getters = new Func<T, object>[props.Length];
for (int i = 0; i < props.Length; i++)
{
var p = props[i];
// Create a compiled lambda (obj => (object)((T)obj).Prop)
var param = Expression.Parameter(typeof(T), "x");
var propertyAccess = Expression.Property(param, p);
Expression converted = Expression.Convert(propertyAccess, typeof(object));
var lambda = Expression.Lambda<Func<T, object>>(converted, param);
getters[i] = lambda.Compile();
}
// Create an IEnumerable<object[]> of rows (deferred streaming)
IEnumerable<object[]> Rows()
{
foreach (var item in dataList)
{
globalProcessedCount++;
var row = new object[props.Length];
for (int i = 0; i < props.Length; i++)
{
row[i] = getters[i](item);
}
// report progress periodically (not for every cell)
if (progress != null && (globalProcessedCount % 10000 == 0))
{
int percent = totalCount > 0 ? (int)((double)globalProcessedCount / totalCount * 100) : 0;
progress.Report(Math.Min(percent, 100));
}
yield return row;
}
// final report
if (progress != null)
progress.Report(100);
}
// Bulk load rows starting at row 2, col 1
workSheet.Cells[2, 1].LoadFromArrays(Rows());
// Column sizing:
// - For small/medium datasets, AutoFitColumns is OK
// - For very large datasets (>100k rows) it is very slow. Here I show a sampling approach:
try
{
if (dataList.Count <= 50000)
{
workSheet.Cells[workSheet.Dimension.Address].AutoFitColumns();
}
else
{
// AutoFit only based on first N rows + header to get reasonable column widths
int sampleRows = Math.Min(2000, dataList.Count);
for (int c = 1; c <= props.Length; c++)
{
var from = workSheet.Cells[1, c, sampleRows + 1, c];
//workSheet.Column(c).Width = from.AutoFitColumnsGetWidth();
// EPPlus internal helper? if not available, skip
// If AutoFitColumnsGetWidth isn't available in your EPPlus, you can skip or set a default width:
// workSheet.Column(c).Width = 20;
}
}
}
catch
{
// If AutoFit fails or is slow, fall back to default width
for (int c = 1; c <= props.Length; c++)
workSheet.Column(c).Width = 20;
}
}
public async Task InitBooksCollectionAsync()
{
BooksCollection = new ObservableCollection<Book>();
List<Book> booksList = new List<Book>();
await Task.Run(async () =>
{
for (int i = 1; i < 3000001; i++)
{
booksList.Add(new Book()
{
Id = idService.GetID(),
Name = nameService.GetName(),
ISBN = isbnService.GetISBN(),
Author = $"Author_{i}",
Comment = $"Comment_{i}",
Content = $"Content_{i}",
Title = $"Title_{i}",
Topic = $"Topic_{i}"
});
if (i % 100000 == 0)
{
await PopulateBooksCollectionAsync(booksList);
}
}
if (booksList.Any())
{
await PopulateBooksCollectionAsync(booksList);
}
LoadingMsg = $"Loading Completed!";
});
}
private async Task PopulateBooksCollectionAsync(List<Book> booksList)
{
var tempList = booksList.ToList();
booksList.Clear();
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
foreach (var bk in tempList)
{
BooksCollection.Add(bk);
}
LoadingMsg = $"Loaded {BooksCollection.Count} items";
}, System.Windows.Threading.DispatcherPriority.Background);
}
[ObservableProperty]
private ObservableCollection<Book> booksCollection;
[ObservableProperty]
private string loadingMsg;
public ICommand ExportInExcelCommand { get; set; }
}
}
//Book.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace WpfApp25.Models
{
public class Book
{
public int Id { get; set; }
public string Author { get; set; }
public string Name { get; set; }
public string Comment { get; set; }
public string Content { get; set; }
public string ISBN { get; set; }
public string Title { get; set; }
public string Topic { get; set; }
}
}
//services.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace WpfApp25.Services
{
public interface IIDService
{
int GetID();
}
public class IDService : IIDService
{
int id = 0;
public int GetID()
{
return Interlocked.Increment(ref id);
}
}
public interface INameService
{
string GetName();
}
public class NameService : INameService
{
int idx = 0;
public string GetName()
{
return $"Name_{Interlocked.Increment(ref idx)}";
}
}
public interface IISBNService
{
string GetISBN();
}
public class ISBNService : IISBNService
{
int idx = 0;
public string GetISBN()
{
return $"ISBN_{Interlocked.Increment(ref idx)}_{Guid.NewGuid():N}";
}
}
}
//DelCommand.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
namespace WpfApp25.Utitlity
{
public class DelCommand : ICommand
{
private Action<Object?> execute;
private Predicate<Object?> canExecute;
public DelCommand(Action<Object?> executeValue, Predicate<Object?> canExecuteValue=null)
{
execute = executeValue;
canExecute = canExecuteValue;
}
public event EventHandler? CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public bool CanExecute(object? parameter)
{
return canExecute == null ? true : canExecute(parameter);
}
public void Execute(object? parameter)
{
execute(parameter);
}
}
}