by Joche Ojeda | Jan 2, 2025 | XtraReports
Introduction ?
If you’re familiar with Windows Forms development, transitioning to XtraReports will feel remarkably natural. This guide explores how XtraReports leverages familiar Windows Forms concepts while extending them for robust reporting capabilities.
? Quick Tip: Think of XtraReports as Windows Forms optimized for paper output instead of screen output!
A Personal Journey ✨
Microsoft released .NET Framework in late 2002. At the time, I was a VB6 developer, relying on Crystal Reports 7 for reporting. By 2003, my team was debating whether to transition to this new thing called .NET. We were concerned about VB6’s longevity—thinking it had just a couple more years left. How wrong we were! Even today, VB6 applications are still running in some places (it’s January 2, 2025, as I write this).
Back in the VB6 era, we used the Crystal Reports COM object to integrate reports. When we finally moved to .NET Framework, we performed some “black magic” to continue using our existing 700 reports across nine countries. The decision to fully embrace .NET was repeatedly delayed due to the sheer volume of reports we had to manage. Our ultimate goal was to unify our reporting and parameter forms within a single development environment.
This led us to explore other technologies. While considering Delphi, we discovered DevExpress. My boss procured our first DevExpress .NET license for Windows Forms, marking the start of my adventure with DevExpress and XtraReports. Initially, transitioning from the standalone Crystal Report Designer to the IDE-based XtraReports Designer was challenging. To better understand how XtraReports worked, I decided to write reports programmatically instead of using the visual designer.
Architectural Similarities ?️
XtraReports mirrors many fundamental Windows Forms concepts:
| Source |
Destination |
| XtraReport Class |
Report Designer Surface |
| XtraReport Class |
Control Container |
| XtraReport Class |
Event System |
| XtraReport Class |
Properties Window |
| Control Container |
Labels & Text |
| Control Container |
Tables & Grids |
| Control Container |
Images & Charts |
| Report Designer Surface |
Control Toolbox |
| Report Designer Surface |
Design Surface |
| Report Designer Surface |
Preview Window |
Like how Windows Forms applications start with a Form class, XtraReports begin with an XtraReport base class. Both serve as containers that can:
- Host other controls
- Manage layout
- Handle events
- Support data binding
Visual Designer Experience ?
The design experience remains consistent with Windows Forms:
| Windows Forms |
XtraReports |
| Form Designer |
Report Designer |
| Toolbox |
Report Controls |
| Properties Window |
Properties Grid |
| Component Tray |
Component Tool |
Control Ecosystem ?
XtraReports provides analogous controls to Windows Forms:
// Windows Forms
public partial class CustomerForm : Form
{
private Label customerNameLabel;
private DataGridView orderDetailsGrid;
}
// XtraReports
public partial class CustomerReport : XtraReport
{
private XRLabel customerNameLabel;
private XRTable orderDetailsTable;
}
Common control mappings:
- Label ➡️ XRLabel
- Panel ➡️ XRPanel
- PictureBox ➡️ XRPictureBox
- DataGridView ➡️ XRTable
- GroupBox ➡️ Band
- UserControl ➡️ Subreport
Data Binding Patterns ?
The data binding syntax maintains familiarity:
// Windows Forms data binding
customerNameLabel.DataBindings.Add("Text", customerDataSet, "Customers.Name");
// XtraReports data binding
customerNameLabel.ExpressionBindings.Add(
new ExpressionBinding("Text", "[Name]"));
Code Architecture ?️
The code-behind model remains consistent:
public partial class CustomerReport : DevExpress.XtraReports.UI.XtraReport
{
public CustomerReport()
{
InitializeComponent(); // Familiar Windows Forms pattern
}
private void CustomerReport_BeforePrint(object sender, PrintEventArgs e)
{
// Event handling similar to Windows Forms
// Instead of Form_Load, we have Report_BeforePrint
}
}
Key Differences ⚡
While similarities abound, important differences exist:
- Output Focus ?️
- Windows Forms: Screen-based interaction
- XtraReports: Print/export optimization
- Layout Model ?
- Windows Forms: Flexible screen layouts
- XtraReports: Page-based layouts with bands
- Control Behavior ?
- Windows Forms: Interactive controls
- XtraReports: Display-oriented controls
- Data Processing ?️
- Windows Forms: Real-time data interaction
- XtraReports: Batch data processing
Some Advices ?
- Design Philosophy
// Think in terms of paper output
public class InvoiceReport : XtraReport
{
protected override void OnBeforePrint(PrintEventArgs e)
{
// Calculate page breaks
// Optimize for printing
}
}
- Layout Strategy
- Use bands for logical grouping
- Consider paper size constraints
- Plan for different export formats
- Data Handling
- Pre-process data when possible
- Use calculated fields for complex logic
- Consider subreports for complex layouts
by Joche Ojeda | Jun 3, 2024 | C#, Data Synchronization
Writing Reusable Tests for SyncFramework Interfaces in C#
When creating a robust database synchronization framework like SyncFramework, ensuring that each component adheres to its defined interface is crucial. Reusable tests for interfaces are an essential aspect of this verification process. Here’s how you can approach writing reusable tests for your interfaces in C#:
1. Understand the Importance of Interface Testing
Interfaces define contracts that all implementing classes must follow. By testing these interfaces, you ensure that every implementation behaves as expected. This is especially important in frameworks like SyncFramework, where different components (e.g., IDeltaStore) need to be interchangeable.
2. Create Base Test Classes
Create abstract test classes for each interface. These test classes should contain all the tests that verify the behavior defined by the interface.
using Microsoft.VisualStudio.TestTools.UnitTesting;
public abstract class BaseDeltaStoreTest
{
protected abstract IDeltaStore GetDeltaStore();
[TestMethod]
public void TestAddDelta()
{
var deltaStore = GetDeltaStore();
deltaStore.AddDelta("delta1");
Assert.IsTrue(deltaStore.ContainsDelta("delta1"));
}
[TestMethod]
public void TestRemoveDelta()
{
var deltaStore = GetDeltaStore();
deltaStore.AddDelta("delta2");
deltaStore.RemoveDelta("delta2");
Assert.IsFalse(deltaStore.ContainsDelta("delta2"));
}
// Add more tests to cover all methods in IDeltaStore
}
3. Implement Concrete Test Classes
For each implementation of the interface, create a concrete test class that inherits from the base test class and provides an implementation for the abstract method to instantiate the concrete class.
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class ConcreteDeltaStoreTest : BaseDeltaStoreTest
{
protected override IDeltaStore GetDeltaStore()
{
return new ConcreteDeltaStore();
}
}
4. Use a Testing Framework
Utilize a robust testing framework such as MSTest, NUnit, or xUnit to ensure all tests are run across all implementations.
5. Automate Testing
Integrate your tests into your CI/CD pipeline to ensure that every change is automatically tested across all implementations. This ensures that any new implementation or modification adheres to the interface contracts.
6. Document Your Tests
Clearly document your tests and the rationale behind reusable tests for interfaces. This will help other developers understand the importance of these tests and encourage them to add tests for new implementations.
Example of Full Implementation
// IDeltaStore Interface
public interface IDeltaStore
{
void AddDelta(string delta);
void RemoveDelta(string delta);
bool ContainsDelta(string delta);
}
// Base Test Class
using Microsoft.VisualStudio.TestTools.UnitTesting;
public abstract class BaseDeltaStoreTest
{
protected abstract IDeltaStore GetDeltaStore();
[TestMethod]
public void TestAddDelta()
{
var deltaStore = GetDeltaStore();
deltaStore.AddDelta("delta1");
Assert.IsTrue(deltaStore.ContainsDelta("delta1"));
}
[TestMethod]
public void TestRemoveDelta()
{
var deltaStore = GetDeltaStore();
deltaStore.AddDelta("delta2");
deltaStore.RemoveDelta("delta2");
Assert.IsFalse(deltaStore.ContainsDelta("delta2"));
}
// Add more tests to cover all methods in IDeltaStore
}
// Concrete Implementation
public class ConcreteDeltaStore : IDeltaStore
{
private readonly HashSet _deltas = new HashSet();
public void AddDelta(string delta)
{
_deltas.Add(delta);
}
public void RemoveDelta(string delta)
{
_deltas.Remove(delta);
}
public bool ContainsDelta(string delta)
{
return _deltas.Contains(delta);
}
}
// Concrete Implementation Test Class
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class ConcreteDeltaStoreTest : BaseDeltaStoreTest
{
protected override IDeltaStore GetDeltaStore()
{
return new ConcreteDeltaStore();
}
}
// Running the tests
// Ensure to use a test runner compatible with MSTest to execute the tests
by Joche Ojeda | Jun 1, 2024 | Data Synchronization, EfCore, SyncFrameworkV2
In modern software development, extending the functionality of a framework while maintaining its integrity and usability can be a complex task. One common scenario involves extending interfaces to add new events or methods. In this post, we’ll explore the impact of extending interfaces within the Sync Framework, specifically looking at IDeltaStore and IDeltaProcessor interfaces to include SavingDelta and SavedDelta events, as well as ProcessingDelta and ProcessedDelta events. We’ll discuss the options available—extending existing interfaces versus adding new interfaces—and examine the side effects of each approach.
Background
The Sync Framework is designed to synchronize data across different data stores, ensuring consistency and integrity. The IDeltaStore interface typically handles delta storage operations, while the IDeltaProcessor interface manages delta (change) processing. To enhance the functionality, you might want to add events such as SavingDelta, SavedDelta, ProcessingDelta, and ProcessedDelta to these interfaces.
Extending Existing Interfaces
Extending existing interfaces involves directly adding new events or methods to the current interface definitions. Here’s an example:
public interface IDeltaStore {
void SaveData(Data data);
// New events
event EventHandler<DeltaEventArgs> SavingDelta;
event EventHandler<DeltaEventArgs> SavedDelta;
}
public interface IDeltaProcessor {
void ProcessDelta(Delta delta);
// New events
event EventHandler<DeltaEventArgs> ProcessingDelta;
event EventHandler<DeltaEventArgs> ProcessedDelta;
}
Pros of Extending Existing Interfaces
- Simplicity: The existing implementations need to be updated to include the new functionality, making the overall design simpler.
- Direct Integration: The new events are directly available in the existing interface, making them easy to use and understand within the current framework.
Cons of Extending Existing Interfaces
- Breaks Existing Implementations: All existing classes implementing these interfaces must be updated to handle the new events. This can lead to significant refactoring, especially in large codebases.
- Violates SOLID Principles: Adding new responsibilities to existing interfaces can violate the Single Responsibility Principle (SRP) and Interface Segregation Principle (ISP), leading to bloated interfaces.
- Potential for Bugs: The necessity to modify all implementing classes increases the risk of introducing bugs and inconsistencies.
Adding New Interfaces
An alternative approach is to create new interfaces that extend the existing ones, encapsulating the new events. Here’s how you can do it:
public interface IDeltaStore {
void SaveData(Data data);
}
public interface IDeltaStoreWithEvents : IDeltaStore {
event EventHandler<DeltaEventArgs> SavingDelta;
event EventHandler<DeltaEventArgs> SavedDelta;
}
public interface IDeltaProcessor {
void ProcessDelta(Delta delta);
}
public interface IDeltaProcessorWithEvents : IDeltaProcessor {
event EventHandler<DeltaEventArgs> ProcessingDelta;
event EventHandler<DeltaEventArgs> ProcessedDelta;
}
Pros of Adding New Interfaces
- Adheres to SOLID Principles: This approach keeps the existing interfaces clean and focused, adhering to the SRP and ISP.
- Backward Compatibility: Existing implementations remain functional without modification, ensuring backward compatibility.
- Flexibility: New functionality can be selectively adopted by implementing the new interfaces where needed.
Cons of Adding New Interfaces
- Complexity: Introducing new interfaces can increase the complexity of the codebase, as developers need to understand and manage multiple interfaces.
- Redundancy: There can be redundancy in code, where some classes might need to implement both the original and new interfaces.
- Learning Curve: Developers need to be aware of and understand the new interfaces, which might require additional documentation and training.
Conclusion
Deciding between extending existing interfaces and adding new ones depends on your specific context and priorities. Extending interfaces can simplify the design but at the cost of violating SOLID principles and potentially breaking existing code. On the other hand, adding new interfaces preserves existing functionality and adheres to best practices but can introduce additional complexity.
In general, if maintaining backward compatibility and adhering to SOLID principles are high priorities, adding new interfaces is the preferred approach. However, if you are working within a controlled environment where updating existing implementations is manageable, extending the interfaces might be a viable option.
By carefully considering the trade-offs and understanding the implications of each approach, you can make an informed decision that best suits your project’s needs.