We've been using Xamarin iOS for the last 8 months and developed a non-trivial enterprise app with many screens, features, nested controls. We've done our own MVVM arch, cross platform BLL & DAL as "recommended". We share code between Android and even our BLL/DAL is used on our web product.
All is good except now in release phase of project we discover irreparable memory leaks everywhere in the Xamarin iOS-based app. We've followed all the "guidelines" to resolve this but the reality is that C# GC and Obj-C ARC appear to be incompatible garbage collection mechanisms in the current way they overlay each other in monotouch platform.
The reality we've found is that hard cycles between native objects and managed objects WILL occur and FREQUENTLY for any non-trivial app. It's extremely easy for this to happen anywhere you use lambdas or gesture recognizers for example. Add in the complexity of MVVM and it's almost a guarantee. Miss just one of these situations and entire graphs of objects will never get collected. These graphs will lure other objects in and grow like a cancer, eventually resulting in a prompt and merciless extermination by iOS.
Xamarin's answer is an uninterested deferral of the issue and an unrealistic expectation that "devs should avoid these situations". Careful consideration of this reveals this as an admission that Garbage Collection is essentially broken in Xamarin.
The realization for me now is that you don't really get "garbage collection" in Xamarin iOS in the traditional c# .NET sense. You need employ "garbage maintanence" patterns actually get the GC moving and doing its job, and even then it'll never be perfect - NON DETERMINISTIC.
My company has invested a fortune trying to stop our app from crashing and/or running out of memory. We've basically had to explicitly and recursively dispose every damn thing in sight and implement garbage maintanence patterns into the app, just to stop the crashes and have a viable product we can sell. Our customers are supportive and tolerant, but we know this cannot hold forever. We are hoping Xamarin have a dedicated team working on this issue and get it nailed once and for all. Doesn't look like it, unfortunately.
Question is, is our experience the exception or the rule for non-trivial enterprise-class apps written in Xamarin?
UPDATE
See answer for DisposeEx method and solution.
I have shipped a non-trivial app written with Xamarin. Many others have as well.
"Garbage collection" isn't magic. If you create a reference that is attached to the root of your object graph and never detach it, it will not be collected. That's not only true of Xamarin, but of C# on .NET, Java, etc.
button.Click += (sender, e) => { ... } is an anti-pattern, because you don't have a reference to the lambda and you can never remove the event handler from the Click event. Similarly, you have to be careful that you understand what you're doing when you create references between managed and unmanaged objects.
As for "We've done our own MVVM arch", there are high profile MVVM libraries (MvvmCross, ReactiveUI, and MVVM Light Toolkit), all of which take reference/leak issues very seriously.
I used the below extension methods to solve these memory leak issues. Think of Ender's Game final battle scene, the DisposeEx method is like that laser and it disassociates all views and their connected objects and disposes them recursively and in a way that shouldn't crash your app.
Just call DisposeEx() on UIViewController's main view when you no longer need that view controller. If some nested UIView has special things to dispose, or you dont want it disposed, implement ISpecialDisposable.SpecialDispose which is called in place of IDisposable.Dispose.
NOTE: this assumes no UIImage instances are shared in your app. If they are, modify DisposeEx to intelligently dispose.
public static void DisposeEx(this UIView view) {
const bool enableLogging = false;
try {
if (view.IsDisposedOrNull())
return;
var viewDescription = string.Empty;
if (enableLogging) {
viewDescription = view.Description;
SystemLog.Debug("Destroying " + viewDescription);
}
var disposeView = true;
var disconnectFromSuperView = true;
var disposeSubviews = true;
var removeGestureRecognizers = false; // WARNING: enable at your own risk, may causes crashes
var removeConstraints = true;
var removeLayerAnimations = true;
var associatedViewsToDispose = new List<UIView>();
var otherDisposables = new List<IDisposable>();
if (view is UIActivityIndicatorView) {
var aiv = (UIActivityIndicatorView)view;
if (aiv.IsAnimating) {
aiv.StopAnimating();
}
} else if (view is UITableView) {
var tableView = (UITableView)view;
if (tableView.DataSource != null) {
otherDisposables.Add(tableView.DataSource);
}
if (tableView.BackgroundView != null) {
associatedViewsToDispose.Add(tableView.BackgroundView);
}
tableView.Source = null;
tableView.Delegate = null;
tableView.DataSource = null;
tableView.WeakDelegate = null;
tableView.WeakDataSource = null;
associatedViewsToDispose.AddRange(tableView.VisibleCells ?? new UITableViewCell[0]);
} else if (view is UITableViewCell) {
var tableViewCell = (UITableViewCell)view;
disposeView = false;
disconnectFromSuperView = false;
if (tableViewCell.ImageView != null) {
associatedViewsToDispose.Add(tableViewCell.ImageView);
}
} else if (view is UICollectionView) {
var collectionView = (UICollectionView)view;
disposeView = false;
if (collectionView.DataSource != null) {
otherDisposables.Add(collectionView.DataSource);
}
if (!collectionView.BackgroundView.IsDisposedOrNull()) {
associatedViewsToDispose.Add(collectionView.BackgroundView);
}
//associatedViewsToDispose.AddRange(collectionView.VisibleCells ?? new UICollectionViewCell[0]);
collectionView.Source = null;
collectionView.Delegate = null;
collectionView.DataSource = null;
collectionView.WeakDelegate = null;
collectionView.WeakDataSource = null;
} else if (view is UICollectionViewCell) {
var collectionViewCell = (UICollectionViewCell)view;
disposeView = false;
disconnectFromSuperView = false;
if (collectionViewCell.BackgroundView != null) {
associatedViewsToDispose.Add(collectionViewCell.BackgroundView);
}
} else if (view is UIWebView) {
var webView = (UIWebView)view;
if (webView.IsLoading)
webView.StopLoading();
webView.LoadHtmlString(string.Empty, null); // clear display
webView.Delegate = null;
webView.WeakDelegate = null;
} else if (view is UIImageView) {
var imageView = (UIImageView)view;
if (imageView.Image != null) {
otherDisposables.Add(imageView.Image);
imageView.Image = null;
}
} else if (view is UIScrollView) {
var scrollView = (UIScrollView)view;
// Comment out extension method
//scrollView.UnsetZoomableContentView();
}
var gestures = view.GestureRecognizers;
if (removeGestureRecognizers && gestures != null) {
foreach(var gr in gestures) {
view.RemoveGestureRecognizer(gr);
gr.Dispose();
}
}
if (removeLayerAnimations && view.Layer != null) {
view.Layer.RemoveAllAnimations();
}
if (disconnectFromSuperView && view.Superview != null) {
view.RemoveFromSuperview();
}
var constraints = view.Constraints;
if (constraints != null && constraints.Any() && constraints.All(c => c.Handle != IntPtr.Zero)) {
view.RemoveConstraints(constraints);
foreach(var constraint in constraints) {
constraint.Dispose();
}
}
foreach(var otherDisposable in otherDisposables) {
otherDisposable.Dispose();
}
foreach(var otherView in associatedViewsToDispose) {
otherView.DisposeEx();
}
var subViews = view.Subviews;
if (disposeSubviews && subViews != null) {
subViews.ForEach(DisposeEx);
}
if (view is ISpecialDisposable) {
((ISpecialDisposable)view).SpecialDispose();
} else if (disposeView) {
if (view.Handle != IntPtr.Zero)
view.Dispose();
}
if (enableLogging) {
SystemLog.Debug("Destroyed {0}", viewDescription);
}
} catch (Exception error) {
SystemLog.Exception(error);
}
}
public static void RemoveAndDisposeChildSubViews(this UIView view) {
if (view == null)
return;
if (view.Handle == IntPtr.Zero)
return;
if (view.Subviews == null)
return;
view.Subviews.ForEach(RemoveFromSuperviewAndDispose);
}
public static void RemoveFromSuperviewAndDispose(this UIView view) {
view.RemoveFromSuperview();
view.DisposeEx();
}
public static bool IsDisposedOrNull(this UIView view) {
if (view == null)
return true;
if (view.Handle == IntPtr.Zero)
return true;;
return false;
}
public interface ISpecialDisposable {
void SpecialDispose();
}
Couldn't be agree more with the OP that "Garbage Collection is essentially broken in Xamarin".
Here's an example shows why you have to always use a DisposeEx() method as suggested.
The following code leaks memory:
Create a class the inherits UITableViewController
public class Test3Controller : UITableViewController
{
public Test3Controller () : base (UITableViewStyle.Grouped)
{
}
}
Call the following code from somewhere
var controller = new Test3Controller ();
controller.Dispose ();
controller = null;
GC.Collect (GC.MaxGeneration, GCCollectionMode.Forced);
Using Instruments you will see that there are ~ 274 persistent objects with 252 KB never collected.
Only way to fix this is add DisposeEx or similar functionality to the Dispose() function and call Dispose manually to ensure disposing == true.
Summary: Creating a UITableViewController derived class and then disposing/nulling will always cause the heap to grow.
iOS and Xamarin have a slightly troubled relationship. iOS uses reference counts to manage and dispose of its memory. The reference count of an object gets incremented and decremented when references are added and removed. When the reference count goes to 0, the object is deleted and the memory freed. Automatic Reference Counting in Objective C and Swift help with this, but it’s still difficult to get 100% right and dangling pointers and memory leaks can be a pain when developing using native iOS languages.
When coding in Xamarin for iOS, we have to bear reference counts in mind as we will be working with iOS native memory objects. In order to communicate with the iOS operating system, Xamarin creates what are known as Peers which manage the reference counts for us. There are two types of Peers – Framework Peers and User Peers. Framework Peers are managed wrappers around well-known iOS objects. Framework Peers are stateless and therefore hold no strong references to the underlying iOS objects and can be cleaned up by the garbage collectors when required – and don’t cause memory leaks.
User Peers are custom managed objects that are derived from Framework Peers. User Peers contain state and are therefore kept alive by the Xamarin framework even if your code has no references to them – e.g.
public class MyViewController : UIViewController
{
public string Id { get; set; }
}
We can create a new MyViewController, add it to the view tree, then cast a UIViewController to a MyViewController. There may be no references to this MyViewController, so Xamarin needs to ‘root’ this object to keep this it alive whilst the underlying UIViewController is alive, otherwise we will lose the state information.
The problem is that if we have two User Peers that reference each other then this creates a reference cycle that cannot be automatically broken – and this situation happens often!
Consider this case:-
public class MyViewController : UIViewController
{
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear (animated);
MyButton.TouchUpInside =+ DoSomething;
}
void DoSomething (object sender, EventArgs e) { ... }
}
Xamarin creates two User Peers that reference each other – one for MyViewController and another for MyButton (because we have an event handler). So, this will create a reference cycle that will not be cleared up by the garbage collector. In order to have this cleared up, we must unsubscribe the event handler, and this is usually done in the ViewDidDisappear handler – e.g.
public override void ViewDidDisappear(bool animated)
{
ProcessButton.TouchUpInside -= DoSomething;
base.ViewDidDisappear (animated);
}
Always unsubscribe to your iOS event handlers.
How to diagnose these memory leaks
A good way to diagnose these memory problems is to add some code in debug to the Finalisers of the classes derived from iOS wrapper classes – such as UIViewControllers. (Although only put this in your debug builds and not in release builds because it’s reasonably slow.
public partial class MyViewController : UIViewController
{
#if DEBUG
static int _counter;
#endif
protected MyViewController (IntPtr handle) : base (handle)
{
#if DEBUG
Interlocked.Increment (ref _counter);
Debug.WriteLine ("MyViewController Instances {0}.", _counter);
#endif
}
#if DEBUG
~MyViewController()
{
Debug.WriteLine ("ViewController deleted, {0} instances left.",
Interlocked.Decrement(ref _counter));
}
#endif
}
So, Xamarin’s memory management is not broken in iOS, but you do have to be aware of these ‘gotchas’ which are specific to running on iOS.
There is an excellent page by Thomas Bandt called Xamarin.iOS Memory Pitfalls that goes into this in more detail and also provides some very useful hints and tips.
I noticed in your DisposeEx method you dispose of the collection view source and table view source before you kill the visible cells of that collection. I noticed when debugging that the visible cells property gets set to an empty array therefore, when you start to dispose visible cells, they no longer "exist" hence it becomes an array of zero elements.
Another thing I noticed is that you will run into inconsistency exceptions if you don't remove the parameter view from its super view, I've noticed especially with setting the layout of the collection view.
Other than that I've had to implement something similar on our side.
Related
I've been stuck on this all day, so I'm going to post everything I've been able to find today that might be useful to helping me, it will be a long post. I'm having 2 issues that I believe are related to the same problem. First, let me explain what I am doing. I have 3 Winforms combo boxes that are bound to lists of all of the devices found by MMDeviceEnumerator. Two output device boxes, one input device. I am using the MMDeviceEnumerator to register a callback for whenever the devices are changed, removed, or a default device is set. The callback fires an event that then invokes a delegate to the form thread to re-enumerate the devices into combo boxes. It looks like this:
public void OnDefaultDeviceChanged(DataFlow dataFlow, Role deviceRole, string defaultDeviceId)
{
Devices.OnDevicesUpdated();
}
//The handler called by this event:
private void UpdateDeviceSelectors(object? sender = null, EventArgs? e = null)
{
Invoke(delegate ()
{
int primaryIndex = Devices.PrimaryOutput + 1, secondaryIndex = Devices.SecondaryOutput + 2, microphoneIndex = Devices.Microphone + 1;
Devices.Refresh();
try
{
SecondaryOutputComboBox.SelectedIndex = secondaryIndex;
PrimaryOutputComboBox.SelectedIndex = primaryIndex;
MicrophoneSelectComboBox.SelectedIndex = microphoneIndex;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
});
}
Now for the two issues I have. The first one involves a semi-random crash that leads back to NAudio.Wasapi.dll, where a System.ExecutionEngineException is thrown. It is kind of easy to reproduce. All I do is change the values of the combo boxes, switch the default devices around, and it will randomly crash.
The second issue occurs when another part of my code is involved. I have a microphone injector, which redirects a WaveInEvent that records a selected input device to a WaveOutEvent, like a loopback. Here is the relevant code for this:
public void Start()
{
if (Soundboard.Devices.SecondaryOutput == -2) return;
micStream = new WaveInEvent();
micStream.BufferMilliseconds = 50;
micStream.WaveFormat = new WaveFormat(44100, WaveIn.GetCapabilities(Soundboard.Devices.Microphone).Channels);
micStream.DeviceNumber = Soundboard.Devices.Microphone;
WaveInProvider waveIn = new(micStream);
var volumeSampleProvider = new VolumeSampleProvider(waveIn.ToSampleProvider());
volumeSampleProvider.Volume = 1 + Settings.Default.MicrophoneGain;
virtualCable = new WaveOutEvent();
virtualCable.DesiredLatency = 150;
virtualCable.DeviceNumber = Soundboard.Devices.SecondaryOutput;
virtualCable.Init(volumeSampleProvider);
micStream.StartRecording();
virtualCable.Play();
}
public void Stop()
{
try
{
if (micStream != null && virtualCable != null)
{
micStream.Dispose();
micStream = null;
virtualCable.Dispose();
virtualCable = null;
}
}
catch
{
micStream = null;
virtualCable = null;
}
}
In the delegate mentioned earlier, I am calling the Stop method of the Mic Injector and then the Start method to refresh the WaveIn and WaveOut devices to use the current device numbers so users do not see a device selected when a different device is being used. When this happens, the program, rather than crashing instantly and inconsistently, always hangs and has to be killed from the task manager. I am certain these 2 problems are related to the same root cause, but I have no idea what that root cause may be. Switching to Wasapi, DirectSound, or ASIO won't work because they lack certain functionalities I need, so I would really like to get this working still using Wave streams.
I've tried to find different ways to detect the device changes, assuming it is an issue deep inside NAudio, but I just can't find anything else. For the second problem specifically, I have moved the calls to the Mic Injector around thinking it may be a threading issue or something, but it didn't work or change the behavior.
I have started working with WPF MVVM Light and now I'am trying to navigate between pages.
In the MainWindow I have added a "BackButton"
<Button Command='{Binding Main.GoBack, Mode=OneWay}' />
which is binding to MainViewModel method "RelayCommand GoBack".
private RelayCommand _goBack;
public RelayCommand GoBack
{
get
{
return _goBack
?? (_goBack = new RelayCommand(
() =>
_navigationService.GoBack();
}));
}
}
Why is this button changing view only once? If I want to click it secound time
it doesn't work (nothing happend). If I change page for another by another button its starting work again and againg only for once.
Part of implementation of FrameNavigationService:
public FrameNavigationService()
{
_pagesByKey = new Dictionary<string, Uri>();
_historic = new List<string>();
}
public void GoBack()
{
if (_historic.Count > 1)
{
_historic.RemoveAt(_historic.Count - 1);
NavigateTo(_historic.Last(), null);
}
}
public void NavigateTo(string pageKey)
{
NavigateTo(pageKey, null);
}
public virtual void NavigateTo(string pageKey, object parameter)
{
lock (_pagesByKey)
{
if (!_pagesByKey.ContainsKey(pageKey))
{
throw new ArgumentException(string.Format("No such page: {0} ", pageKey), "pageKey");
}
var frame = GetDescendantFromName(Application.Current.MainWindow, "MainFrame") as Frame;
if (frame != null)
{
frame.Source = _pagesByKey[pageKey];
}
Parameter = parameter;
_historic.Add(pageKey);
CurrentPageKey = pageKey;
}
}
What can I do to handle this? May be I should do it tottaly differently?
You should possibly not be doing goback at all.
Unless you really want to use the journal, using a frame and pages is a bad idea. It's a rare requirement to go back to the last view in desktop apps. What with them not being a web browser.
Maybe you have that requirement though.
If you have a frame then you have it's journal and you can just call goback on the frame's navigationservice.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.navigation.navigationservice.goback?view=netframework-4.8
You set keepalive on pages.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.page.keepalive?view=netframework-4.8
You wrote that code and it seems to be largely reproducing navigationservice functionality. From what you've shown us.
As it is.
Use type rather than a magic string as the key. A type is checked at compile time, a magic string is not and you can make mistakes.
Have you explored this issue at all? I think maybe this is one of those times that telling someone what they did wrong isn't really helping as much as telling them how they ought to diagnose.
Debugging is a key skill for any developer.
You have the code running in front of you.
Put break points in, step through and examine what is happening.
When you navigate, what ends up in _historic?
When you goback, what happens exactly?
When you click the goback that second time what path does it go down and what state is causing that.
Make sure you are using RelayCommand in GalaSoft.MvvmLight.CommandWpf,not at GalaSoft.MvvmLight.Command.RelayCommand
All -
I have currently have a POC WPF project which works end to end. The application simulates real-time market data being published through a library (Publisher) and my WPF client is Subscriber (has the handler method). It uses Custom Events to publish data.
Question I have is this:
1) I want to implement Producer Consumer - so my handler doesn't pull the data into Observable Collection directly.
2) I precisely know how to implement Producer/Consumer C# snippets (http://msdn.microsoft.com/en-us/library/hh228601.aspx) but wanted to more understand how this will fit in my current architecture. Here is a diagram
3) Can anybody help me out with code approach, links etc.
MainWindowViewModel.cs
public class MainWindow_VM : ViewModelBase
{
#region Properties
public myCommand SbmtCmd { get; set; }
public ObservableCollection<StockModel> stocks { get; set; }
#endregion
#region Fields
private readonly Dispatcher currentDispatcher;
#endregion
public MainWindow_VM()
{
SbmtCmd = new myCommand(mySbmtCmdExecute, myCanSbmtCmdExecute);
currentDispatcher = Dispatcher.CurrentDispatcher;
stocks = new ObservableCollection<StockModel>();
}
private void mySbmtCmdExecute(object parameter)
{
MarketDataProvider p = new MarketDataProvider();
p.OnMarketData += new EventHandler<MarketDataEventArgs>(handlermethod);
p.GenerateMarketData();
}
private bool myCanSbmtCmdExecute(object parameter)
{
return true;
}
// Subscriber method which will be called when the publisher raises an event
private void handlermethod(object sender, MarketDataEventArgs e)
{
foreach (Stock s in e.updatedstk)
{
StockModel sm = new StockModel();
sm.symbol = s.symbol;
sm.bidprice = s.bidprice;
sm.askprice = s.askprice;
sm.lastprice = s.lastprice;
currentDispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate()
{
if (sm != null)
{
if (stocks.Any(x => x.symbol == sm.symbol))
{
var found = stocks.FirstOrDefault(x => x.symbol == sm.symbol);
int i = stocks.IndexOf(found);
stocks[i] = sm;
}
else
{
stocks.Add(sm);
}
}
});
}
}
}
I have done some projects with market feeds and your chart looks fine conceptually. To avoid scalability issues, or to design proactively against scalability issues, you can consider making your producer/consumer box have multiple instances to accommodate multiple feeds and/or multiple instruments within the feed. If, for example, a given market becomes densely volatile, you don't want all the other instruments starved for data.
Also, some people like to switch feeds for a given instrument based upon arbitrary criteria, like getting YEN from London until the gold fix, and then switching to NYC, and then again switching to Tokyo.
The other thing I can mention is for the arrow going out of the producer/consumer box to pass POCO DTO's only. It adds to the value of your application and also makes isolation testing a lot easier.
Testing off live feeds (or even simulated feeds) is scant because they don't capture all the conditions that need to be tested before an app is deployable.
Finally I would mention that the producer/consumer pattern was implemented starting in .NET 4.0 with the System.Collections.Concurrent name space... http://msdn.microsoft.com/en-us/library/dd287147.aspx I have been using these classes in production and they really cut through the need to test a home-grown design pattern.
I've created a UI toolkit called ReactiveTables which allows you to create live tables which can be joined, filtered, extended with calculated columns and then bound to WPF controls. The tables expose an IObservable interface and have been designed for performance.
You can hook the tables up directly to your producer/consumer implementation and then bind them to your views. The tables will notify on changes to individual cells and there is a helper class for translating this into INotifyPropertyChanged events.
On the receiving side there are classes which can throttle updates to the UI tables and marshal form your data threads to the UI thread.
I'm implementing a Unity PRISM module on demand using IModuleManager. Suppose there are multiple points where functionality from the module is required. Am I right to call moduleManager.LoadModule("MyModule") at each of these points even if the module might have already loaded previously? It's not going to reload is it?
And is this best practice?
Thanks
When You look at Prism Modularity Quickstart
You will find this method which is responsible for loading Module
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (!e.Handled)
{
if ((this.moduleTrackingState != null) && (this.moduleTrackingState.ExpectedInitializationMode == InitializationMode.OnDemand) && (this.moduleTrackingState.ModuleInitializationStatus == ModuleInitializationStatus.NotStarted))
{
this.RaiseRequestModuleLoad();
e.Handled = true;
}
}
}
Please note that there is a condition preventing call to this.RaiseRequestModuleLoad(); when ModuleInitializationStatus is not ModuleInitializationStatus.NotStarted.
I tried to get rid of that and discovered following:
There is no exception thrown when you call moduleManager.LoadModule("YourModule"); multiple times.
Module's Initialize method is called only the first time you load the module.
Module loading means that the module assembly is transferred from disk into memory. So... If it is reloaded every time You really shouldn't do that. Just to answer your question "Is this best practice?" I have to say... It's most certainly NOT. I am not able to tell you whether it's reloaded each time you call moduleManager.LoadModule("YourModule"); but IMHO You should load module only once simply because they do it once as well.
Agreeing with Viktor, the response in this thread suggests not to call LoadModule but to check the ModuleState using IModuleCatalog and IModuleManager.
Here is the code snippet from that post:
(...)
var module = this.moduleCatalog.Modules.FirstOrDefault(m => m.ModuleName == "MyModule");
if (module != null)
{
if (module.State != ModuleState.Initialized)
{
moduleManager.LoadModuleCompleted += moduleManager_LoadModuleCompleted;
moduleManager.LoadModule("MyModule");
}
else
{
//Initialization logic
}
}
}
void moduleManager_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
{
moduleManager.LoadModuleCompleted -= moduleManager_LoadModuleCompleted;
if (e.ModuleInfo.ModuleName == "MyModule")
{
//Initialization logic
}
}
(...)
I want to know what's the best practice for the following case :
I'm developing an application that need to be used on three different screen size : 240x320 ; 800x600 ; 1280x1024 (and only these three) in different languages (using localization).
So what I've done is to create a library by screen size, each implementing an interface defined in the project that calls these screens.
First question is, all my resources files are duplicated and that is many files to keep up to date (or with duplicate labels). Is there a simple way to change this?
Second question is, am I using the good approach or exists it a better way to do what I'm trying to do? By applying a design pattern maybe?
Sample code
The caller code :
public int X { get { return 800; } }
public int Y { get { return 600; } }
public interface IScreenForm
{
Form FormComponent { get; }
}
public interface IFormA : IScreenForm
{
TextBox UserTextBox { get; } // For use
}
public void LoadForm<T>()
where T:IScreenForm
{
if (!typeof(T).IsInterface)
{
throw new ArgumentException(typeof(T).Name + " is not an interface");
}
Assembly screenAssembly;
string screenResolutionDll = string.Format("{0}_{1}_screens.dll", this.X, this.Y);
screenAssembly = Assembly.LoadFrom(screenResolutionDll);
Type formType = screenAssembly.GetTypes()
.FirstOrDefault(t => t.GetInterfaces().Where(i => i.Name == typeof(T).Name).Count() > 0);
if (formType != null)
{
Form form = (Form)formType.GetConstructor(new Type[] { }).Invoke(null);
if (form != null)
{
form.Show();
}
else
{
throw new ArgumentException("Form doesn't provide a new() ctor");
}
}
else
{
throw new ArgumentException("Any form doesn't implement the interface : " + typeof(T).Name);
}
}
Screen DLL :
public partial class MyFirstForm : Form, caller.IFormA
{
public Form1()
{
InitializeComponent();
}
/* caller.IFormA interface implementation */
}
Arnaud, from my own experience, from reading, and from talking to experienced developers: When it comes to supporting multiple screen sizes and with localization, there are no magic bullets. For localization put all your strings in resource files, or even in data files.
As far as the screen sizes, I would not try to be too clever. Yes, make sure that none of your business / non-GUI logic gets duplicated, but duplicating resource files, forms, etc - I would not worry about it. Some GUI frameworks like Qt and GTK auto-resize and auto-position GUI-widgets (e.g. Qt has 'spacer' widgets). It works OK most of the time, however I still prefer explicit control. With GUI programming, there are often unexpected glitches, having three independent sets of GUI components will allow you to address them, should they arise. Some example sources of problems:
1) Font sizing.
2) Windows accessibility settings.
3) Some national languages have longer average words than others, the long words have trouble fitting in the real estate available.
If I were in your shoes, I would look for example how browsers handle this (e.g. mobile vs. desktop version) and I would try to find some practical advice on the Web (like here at SO). I doubt that books on design patterns will help much with this. Disclaimer: I am a design-pattern skeptic.
It is hard to advice without getting the whole picture but for some tips that might by handy read this
http://msdn.microsoft.com/en-us/library/ff648753.aspx
about the SmartClient software Factory.
It comes with architecural guidance and solutions for issue you often see in this kind of apps