We have a Silverlight application. It has a couple of pages that sit inside tabs in our UI. In the past, we've called them SavePage, and PanelPage. Save page just has basic functionality for editing the detail of a record, creating new records, and deleting the existing record on screen. PanelPage inherits from SavePage. PanelPage is a little more sophisticated in that panels become visible/invisible based on selections you make on screen.
The code was a huge mess in the Silverlight application. But, recently, I took the step of porting this code to work in Xamarin Forms. I made two failed attempts to do this, and on my third attempt, I got the code to work on all targeted platforms: Silverlight, iOS, Android, and Windows UWP. I'm fairly happy with the class design for now. It could be simpler, but this will do the trick for a while.
The point of this design is that the UI logic is abstracted away from the physical UI controls themselves. I removed the System.Windows usings away from the shared code that sits across both platforms (SavePage, and PanelPage). These pages are more like "Controllers" in the MVC, or MVVC patterns. But, I don't think that what I have created is exactly either of those two patterns. However, I had to split these classes up in to two parts: one for abstract UI calls like SaveAsync(), and one for platform specific UI calls like ReportError. I'm struggling to get the naming of the classes right since I don't even know what design pattern I am using.
Here is a class diagram:
Here is the code for some of the interfaces concerned:
public interface IPage : IRecordSelector
{
/// <summary>
/// This event should be raised when the busy state of a tab changes
/// </summary>
event EventHandler<BusyStateChangedEventArgs> BusyStateChanged;
object PageElement { get; }
}
public interface IButtonDrivenPage : IPage
{
Task SaveClickAsync();
Task DuplicateClickAsync();
Task DeleteClickAsync();
Task NewClickAsync();
event EventHandler<ButtonVisibilityChangedEventArgs> ButtonVisibilityChanged;
IRecord GetRecord();
}
public interface ISavePage : IButtonDrivenPage, IRequestClose
{
string DataContextXmlSnapshot { get; }
bool PromptForChangeCancel { get; }
IRecord SelectedItem { get; }
Task SetSelectedItemAsync(IRecord selectedItem);
event EventHandler SelectedItemChanged;
void Close();
void SetAutomationObject(object automationObject);
ISavePageUIController SavePageUIController { get; }
}
public interface ISavePageUIController: IDisposable
{
/// <summary>
/// The UI controller is notifying the page that the UI content has been loaded
/// </summary>
event EventHandler ContentLoaded;
/// <summary>
/// Prompt the user for a yet or a no
/// </summary>
Task<bool> GetYesNoFromPrompt(string message, string title);
/// <summary>
/// Report an error to the user
/// </summary>
void ReportError(string title, string message, Exception exception);
/// <summary>
/// Notifies the UI that the DataContext/Binding context has changed
/// </summary>
void SetSelectedItem(IRecord selectedItem);
/// <summary>
/// The actual UI object that is displayed on screen as the content of the page
/// </summary>
object PageElement { get; }
/// <summary>
/// Clears residual errors from the screen if they exist
/// </summary>
void ClearErrors();
/// <summary>
/// The record was saved. The selectedItem parameter will be the saved record from the server.
/// </summary>
void CurrentRecordSaved(IRecord selectedItem);
/// <summary>
/// This event occurs when the UI wants to notify the controller that a Save button has been clicked in the UI somewhere
/// </summary>
event EventHandler SaveClicked;
}
public interface IPanelUIController : ISavePageUIController
{
void CreateAndAddPanelFromContent(PagePanel pagePanel, double? panelHeight);
IEnumerable<IPagePanel> GetIPagePanelControls();
void SetHeader(IPanelHeader pageHeader);
void SetVisiblePanels(IList<bool> visiblePanels);
void HideAllPanels();
event EventHandler<RecordsSelectedRoutedEventArgs> PanelPageRecordsSelected;
}
These interfaces have been implemented successfully in Silverlight and Xamarin Forms. So, is this similar to another UI design pattern? Can anyone recommend improvements? Or, tell me what I'd need to do to convert this to a more standard UI design pattern? How about naming? What should I name my classes and interfaces here?
To be honest, I wouldn't be too obsessed with being one or the other (MVC, MVVM or MVP), they're pretty much the same and the point is to get keep that "big split". That said, right now, IMHO you seem to be closest to MVP (Model View Presenter)
The problem is that you have a lot of intermingled logic there: IPage should really just be a View but you have it doing things that a controller typically would. Same with it's children: ISavePagehas a method called SetAutomation object that I would normally expect to see in a Controller (at least if I'm guessing correctly on it's function).
Adam Freeman did a great job talking about how to break this stuff up in ASP.NET MVC 5: http://enos.itcollege.ee/~ijogi/Nooks/Pro%20ASP.NET%20MVC%205/Pro%20ASP.NET%20MVC%205.9781430265290.pdf Check out page 51 where he breaks down what each item should be conceptually, which may help?
I would try doing it this way, given that we've established that your IPage is really a Controller.
public class PageElement
{
IPageController _controller;
// these are your models - your controller will simply allow them to be shown with your other methods
private PageData _data;
private PageData _otherData;
public PageElement(IPageController ctrl)
{
_controller = ctrl;
}
}
public class PageController : IPageController
{
IPageService _service;
public PageController(IPageService service)
{
_service = service;
}
// this is what your button calls when clicked
public void SaveAsync(object sender, SomeEventArgs args)
{
// the service does the actual work
_service.SaveAsync()
}
}
public class PageService : IPageService
{
public void SaveAsync(){ // what it does}
}
Look like a MVC, or previous controller-view pattern (prior to MVC)
Related
I have a Xamarin Forms app that I want to read NFC tags on. I've made an interface called INFC for reading the tags.
/// <summary>
/// This interface defines NFC relating functions that are cross platform.
/// </summary>
public interface INFC
{
/// <summary>
/// Inits the object.
/// </summary>
void Init();
/// <summary>
/// Starts the process for scanning for the included platform.
/// </summary>
/// <param name="tagInformation">Optional related tag information that you may need for the scan.</param>
void StartNFCScan(object tagInformation = null);
/// <summary>
/// Called when the tag is finished scanning and we have the content.
/// </summary>
event EventHandler<String> TagScanned;
}
I created the following Android specific implementation.
[assembly: Dependency(typeof(INFCImplementation))]
namespace Test.Droid.Models
{
/// <summary>
/// The android implementation of the NFC platform.
/// </summary>
public class INFCImplementation : INFC
{
public event EventHandler<String> TagScanned;
public static NfcAdapter adapter { get; set; }
/// <summary>
/// Called to init the object.
/// </summary>
public void Init()
{
//Set the adapter.
adapter = NfcAdapter.GetDefaultAdapter(Forms.Context);
}
/// <summary>
/// Starts the process for scanning for the included platform.
/// </summary>
/// <param name="tagInformation">Optional related tag information that you may need for the scan.</param>
public void StartNFCScan(object tagInformation = null)
{
//Create a variable to hold the tag content.
String tagContent = null;
try
{
//Process the NDEF tag and get the content as a String.
tagContent = "http://stackoverflow.com";
}
catch (Exception e)
{
}
//Raise the tag content with the scanned event.
TagScanned?.Invoke(this, tagContent);
}
}
}
My Main Activity is as follows.
/// <summary>
/// The main activity for the app.
/// </summary>
[Activity(Label = "Test", Icon = "#drawable/icon", Theme = "#style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
INFCImplementation nfcImplementation;
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
//Enable experimental fast renderers.
Forms.SetFlags("FastRenderers_Experimental");
Forms.Init(this, bundle);
//Load up the zxing framework.
ZXing.Net.Mobile.Forms.Android.Platform.Init();
//Load up the user dialogs plugin.
UserDialogs.Init(() => (Activity)Forms.Context);
//Init the tinted image renderer.
TintedImageRenderer.Init();
//Store our NFC interface class.
nfcImplementation = DependencyService.Get<INFCImplementation>() as INFCImplementation;
//Init our NFC interface.
nfcImplementation.Init();
LoadApplication(new App());
}
protected override void OnResume()
{
//Call the base method.
base.OnResume();
//Create the intent for NFC reading.
Intent intent = new Intent(this, GetType()).AddFlags(ActivityFlags.SingleTop);
//Start a dispatch on our NFC adapter.
INFCImplementation.adapter?.EnableForegroundDispatch
(
this,
PendingIntent.GetActivity(this, 0, intent, 0),
new[] { new IntentFilter(NfcAdapter.ActionTechDiscovered) },
new String[][]
{
new string[]
{
"android.nfc.tech.Ndef"
},
new string[] {
"android.nfc.tech.MifareClassic"
},
}
);
}
protected override void OnPause()
{
//Call the base method.
base.OnPause();
//Stop the dispatch on our NFC adapter.
INFCImplementation.adapter?.DisableForegroundDispatch(this);
}
protected override void OnNewIntent(Intent intent)
{
//Call the base method.
base.OnNewIntent(intent);
//Check if this is the NFC intent.
if (intent != null && (NfcAdapter.ActionNdefDiscovered.Equals(intent.Action) || NfcAdapter.ActionTechDiscovered.Equals(intent.Action) || NfcAdapter.ActionTagDiscovered.Equals(intent.Action)))
{
var test = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Tag;
nfcImplementation.StartNFCScan(test);
}
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
//Call the base method.
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
//Check with the permissions plugin.
PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
//Check with the zxing plugin.
ZXing.Net.Mobile.Android.PermissionsHandler.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
In my view model for the mainpage that is the binding context for the main page I add the following in the constructor.
/// <summary>
/// Constructs the scanner view model with the scanner view we want to use.
/// </summary>
public ScannerPageViewModel()
{
//Subscribe to the tag scanned event.
CrossNFC.Current.TagScanned += ProcessNFCScanResult;
}
private void ProcessNFCScanResult(object sender, string e)
{
SetLabel(e);
}
Ok so for the issue. I believe that this should make it so the OnNewIntent function will call the start NFC scan on the interface and then that will call the event which will fly all the way up to the view model and allow me to handle the content. I do this as I only want to scan NFC tags on one page in the app ONLY while the app is in the foreground. Everytime I get to the invoke call, the TagScanned event is null.
Placing breakpoints around I found that the following occurs when I scan a tag:
MainActivity OnPause Gets Called -> Scanner Page OnDisappearing Gets Called -> OnNewIntent Gets Called and calls the null event -> MainActivity OnResume gets called -> Scanner Page OnAppearing Gets Called
I believe that the OnDisappearing call is making the event unable to process. I based a lot of my code on the NFCForms Github project (https://github.com/poz1/NFCForms) which when downloading the sample project and running it DOES NOT trigger OnDisappearing and OnAppearing. It just calls OnPause, OnNewIntent, and OnResume and the event gets to his page.
Why is my page getting unloaded and the event doesn't get called? If I'm doing something wrong, how can I notify my ViewModel for the specific page when a tag was scanned? I'm thinking this is either an issue with the way I'm making the intent request for NFC or something not related to NFC in which I'm processing view events wrong due to the sample NFCForms application working properly on the same phone.
Edit
I made a completely new project with the same basic code and it worked the way I believe it should have. Now I'm trying to figure out why OnAppearing and OnDisappearing is called on the pages.
Edit 2
I found out that OnAppearing and OnDisappearing gets called if the pages are wrapped in a navigation page. That is why the new project which is a single view did not call it and when I added the navigation page it did call it.
HOWEVER even when changing my project to a single page, the old project I'm working on still had the event as null, while the new test project had the event as valid.
So I'm thinking that somehow I'm not doing events right?
Would this be of some use to your case ?
// In ScannerPage
protected override void OnAppearing ()
{
base.OnAppearing ();
MessagingCenter.Subscribe<string>(this, "eventName", (label) => {
// do something whenever the message is sent
Device.BeginInvokeOnMainThread (() => {
MyScannerPageViewModel.SetLabel(label);
});
});
}
protected override void OnDisappearing ()
{
base.OnDisappearing ();
MessagingCenter.Unsubscribe<string> (this, "eventName");
}
And in the MainActivity chose where you would like to put this line
Xamarin.Forms.MessagingCenter.Send("LabelName","eventName");
EDIT: Changed the code a bit
Last successful post was Jan 2018.
Anyone have this working in 2019? Every test I try the anonymous MessagingCenter message is never heard by the subscriber.
Same use-case as the original poster:
Send the message from MainActivty of Android, listen for it in a ViewModel of the shared/agnostic layer.
In my tests, the anonymous message isn't even heard within the same class, or same layer.
UPDATE:
Further input from the community found a solution: You have to specify the type Application and Application.Current.
https://forums.xamarin.com/discussion/comment/370364#Comment_370364
So the previously working syntax seems broken in 2019 - there is a way around it.
Android layer (MainActivity or Broadcast listener):
MessagingCenter.Send(Xamarin.Forms.Application.Current,"ISENGINEON", result.ToString());
Shared layer view model
MessagingCenter.Subscribe<Application,string>(this,"ISENGINEON",OnEngineOnChanged);
While an answer like this makes me very sad, it did end up working.
Since I found making a new project worked, I made a new forms project with the same name as mine and then removed the Android project from my solution and replaced it with the new one. I then reinstalled all the nuget packages and copied and pasted all my code in the new project.
And it works now.....
So I'm guessing something along the way broke something in the core of the VS project or something. I hate kinda hand wavy answers but this is what worked for me. All the code I posted above was unchanged and it started working.
I'm looking for the right way to dispose objects in a Xamarin Forms application. Currently i'm using XAML and MVVM coding style. Then from my view model i get a reference to a disposable object through the builtin service locator (DependencyService). Ideally i should be able to call Dispose() on the objects from my view model, but other solutions like attaching to ContentPage.OnDisappearing and NavigationPage.Popped could be feasible.
I had pretty much the same requirement a couple of weeks ago. I wanted to make sure that event subscriptions in my view models would be unsubscribed when the page is closed. After a lot of research my conclusion was that the simplest solution was to use the ContentPage.OnDisappearing method.
As you pointed out the object you want to dispose is in your ViewModel, so you need a little bit of infrastructure to make sure your ViewModel is informed when the it's disappearing. To do that I defined a base implementation of my view model that had two key methods OnAppearing and OnDisappearing (note this was a class rather than an interface because I have other base functionality such as IPropertyNotify implementation - not shown here).
public class ViewModelBase
{
/// <summary>
/// Called when page is appearing.
/// </summary>
public virtual void OnAppearing()
{
// No default implementation.
}
/// <summary>
/// Called when the view model is disappearing. View Model clean-up should be performed here.
/// </summary>
public virtual void OnDisappearing()
{
// No default implementation.
}
}
Then I subsclassed ContentPage and override the OnAppearing and OnDisappearing methods and then use them to notify my view model.
public class PageBase : ContentPage
{
/// <summary>
/// Performs page clean-up.
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
var viewModel = BindingContext as ViewModelBase;
// Inform the view model that it is disappearing so that it can remove event handlers
// and perform any other clean-up required..
viewModel?.OnDisappearing();
}
protected override void OnAppearing()
{
base.OnAppearing();
// Inform the view model that it is appearing
var viewModel = BindingContext as ViewModelBase;
// Inform the view model that it is appearing.
viewModel?.OnAppearing();
}
}
Then when you implement a page just make sure that it is of type PageBase:
<?xml version="1.0" encoding="utf-8" ?>
<pages:PageBase xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Forms.App.Controls;assembly=Forms.App"
xmlns:converters="clr-namespace:Forms.App.Converters;assembly=Forms.App"
xmlns:pages="clr-namespace:Forms.App.Pages;assembly=Forms.App"
x:Class="Forms.App.Pages.LogonPage"
NavigationPage.HasNavigationBar="False"
Title="Logon">
And in your ViewModel you can then override your OnDisappearing method and dispose your objects:
public class FormViewModel : ViewModelBase
{
public override void OnDisappearing()
{
base.OnDisappearing();
// Dispose whatever objects are neede here
}
}
Just one thing to watch out for - if you're using stack navigation the OnDisappearing method gets called when you stack another page on-top of your current page (your page is disappearing temporarily after all). So you will need to cater for this and probably not dispose your object in that case. However if you're not stacking anything on-top of your page there is nothing to worry about. In my case it was just event subscriptions so I attached the event handlers in the OnAppearing and detached them on the OnDisappearing.
I hope that helps you out!
We were getting disposed of object exceptions in Forms when Bindings to ListViews or Labels changed values as pages/fragments were being disposed of. I'm assuming you could dispose of objects in your ViewModel the same place we were removing bindings.
protected override void OnParentSet()
{
base.OnParentSet();
if (Parent == null)
{
//Clear a bunch of bindings or dispose of ViewModel objects
BindingContext =
_listView.ItemsSource = null;
}
}
I have View Models that conform to IDisposable. So I needed a way for the Page's BindingContext to be disposed when the page is no longer needed.
I used the suggestion of Nick which uses OnParentSet being set to NULL to known when the page is no longer needed.
The SafeContentPage class can be used in place of ContentPage. Iff The binding context supports IDisposable will it automatically try to dispose the binding context.
public class SafeContentPage : ContentPage
{
protected override void OnParentSet()
{
base.OnParentSet();
if (Parent == null)
DisposeBindingContext();
}
protected void DisposeBindingContext()
{
if (BindingContext is IDisposable disposableBindingContext) {
disposableBindingContext.Dispose();
BindingContext = null;
}
}
~SafeContentPage()
{
DisposeBindingContext();
}
}
The OnDisappearing method isn't a reliable technique as there are platform differences in terms of when it is called, and just because the page disappeared doesn't mean its View Model is no longer needed.
I have this requirement to create a REST API to manipulate a database. I decided on WCF Data Services v5.6 because i don't want to rewrite the wheel and i think that way is becoming the standard.
BUT, i need to apply business rules to the objects. All entities involved derive from a base class that has control fields like IsDeleted and so that need checking for example, against a select/GET.
My design has 4 projects:
DomainModel: Contains only the POCO entities (made by separating the Model.tt into a new project
DataAccessLayer: Contains the Context.tt that generates the EventsDomainModel context class
BusinessLayer: Contains a custom DbContext that does the validation, more on this in a moment
RestApi: The website and the services.
Currently, this is the validation i have:
public class GenericBusinessValidator<T>:DbContext where T: class, IBaseEntity
{
private DbContext _ctx;
private DbSet<T> _set;
/// <summary>
/// Standard constructor
/// </summary>
/// <param name="context">The DbContext object</param>
public GenericBusinessValidator(DbContext context)
{
_ctx = context;
_set = _ctx.Set<T>();
}
public IEnumerable<T> GetAll()
{
return _set.Where(x => x.IsActive == true);
}
}
and the code needed to make the service work (Events.svc) is the standard
public class Events : EntityFrameworkDataService<EventsDomainModel>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
// Examples:
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
}
Now, what i am trying to achieve is to REPLACE the EventsDomainModel class wich is the vanilla DbContext -derived class from the second piece of code with the first, that does the validation but i dont know how;
The way i understand, my validator deals with a specific DbSet inside a given DbContext; the code from the WCF service needs a specific dbContext.
So, how can i validate all DbSets without particularizing my class, ie, avoid making
public class GenericBusinessValidator<T>:DbContext where T: class, IBaseEntity
{
private DbContext _ctx;
private DbSet<T> _set;
/// <summary>
/// Standard constructor
/// </summary>
/// <param name="context">The DbContext object</param>
public GenericBusinessValidator(DbContext context)
{
_ctx = context;
_set = _ctx.Set<T>();
}
private DbSet<Venue> Venues;
private DbSet<EventCategory> Categories;
...
...
...
BASICALLY, what i'm trying to accomplish is
make sure that a call like
http://localhost/Events.svc/EventCategories?$format=application/json
returns me all EventCategories that have isActive=true (applied generically) WITHOUT resorting to ServiceOperations and thus avoiding defeating the purpose of using REST
As it turns out, what i needed were QueryInterceptors and ChangeInterceptors
https://msdn.microsoft.com/en-us/library/dd744842(v=vs.110).aspx
I am working on a WPF application in which a user may initiate a process by pushing a button on the UI. The user may then be prompted with a sequence of actions that they must carry out to complete the process. The view is responsible for passing the initial request to initiate the process down to the domain. The view is also responsible for DISPLAYING the steps that the user must perform to complete the process.
The domain, on the other hand, is squarely response for working out WHAT steps must be carried out by the user. The domain is also capable of detecting when a user has completed the requested step.
If a user initiates a process, and that process requires them to perform some physical action, then I would like a box to pop-up with a message describing what they must do. When the action has been completed, it is detected by the domain, and the window should automatically close.
Passing requests from the View down to the Domain is simple. I do this using the wpf ICommand pattern. It is passing information back the other way that I am finding challenging. I am aware of bindings and the INotifyProperyChanged interface, but I do not feel that this is a good fit for what I am trying to do.
So, here is my initial attempt...
This interface is implemented by the View and consumed by the Domain. It allows the domain to communicate with the user;
public interface IUserRequestMedium
{
/// <summary>
/// Ask the user to perform an action. User does
/// not need to provide any feedback via the user
/// interface, since it is possible for the
/// application to detect when the action has been
/// carried out by the user. The dialog will be closed
/// when either the requested action has been detected,
/// or the user aborts.
/// </summary>
/// <param name="message">
/// Request to be displayed to the user.
/// </param>
/// <param name="userAbortCallback">
/// Callback invoked by the view when the user cancels
/// the request.
/// </param>
/// <param name="actionDetectedCallback">
/// Callback invoked by the domain to confirm the
/// that the requested action has been completed.
/// </param>
void AskUserToPerformDetectableAction(
string message, Action userAbortCallback,
out Action actionDetectedCallback);
}
Here is the View code-behind. Some of this code was taken from tutorials (and subsequently mangled) on the web. It's not working, but I hopefully it communicates my intent.
public partial class MainWindow : Window, IUserRequestMedium
{
// Constructor and other stuff...
public void AskUserToPerformDetectableAction(
string message, Action userAbortCallback,
out Action actionDetectedCallback)
{
Action closeWindow;
NewWindowThread(
() => new ActionRequestBox(message, userAbortCallback),
out closeWindow);
actionDetectedCallback = closeWindow;
}
private Window newWindow;
private void NewWindowThread(
Func<Window> construction,
out Action closeWindow)
{
var thread = new Thread(() =>
{
newWindow = construction();
newWindow.Show();
newWindow.Closed += (sender, e) => newWindow.Dispatcher.InvokeShutdown();
System.Windows.Threading.Dispatcher.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
Window rememberedWindow = newWindow;
closeWindow = () =>
{
if (rememberedWindow != null)
rememberedWindow.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
new Action(Close));
};
}
}
And here's a usage example from the domain;
public class SomeDomainClass
{
IUserRequestMedium userRequestMedium; // assume this has been assigned in constructor
private Action notifyUserOfActionDetected;
public void PerformSomeProcess()
{
bool processCannotBeCompletedWithoutPowerCycle = ...; // some logic
if (processCannotBeCompletedWithoutPowerCycle)
{
userRequestMedium.AskUserToPerformDetectableAction(
"Please cycle the power on the external device",
CancelProcess,
out notifyUserOfActionDetected);
}
}
public void CancelProcess()
{
// User doesn't want to perform the required action
// so process must be aborted...
}
private void OnPowerCycleDetected()
{
notifyUserOfActionDetected();
}
}
How can I make this work? It is the cross-threading aspect that I am getting caught on. I have not been successful in making the window automatically close when the action is detected by the domain.
Or, taking a step backward, is there a better approach to solve this problem?
After learning a little about Dispatcher.Invoke, this is what I ended up with. So far it seems to be working pretty well.
private Window activeRequestBox;
// invoked on domain thread
public void AskUserToPerformDetectableAction(
string message, Action userAbortCallback,
out Action actionDetectedCallback)
{
OpenDetectableActionRequestBox(message, userAbortCallback);
actionDetectedCallback = CloseRequestBox;
}
private void OpenDetectableActionRequestBox(
string message, Action userAbortCallback)
{
Action openWindow =
() =>
{
activeRequestBox = new DetectableActionRequestBox(
message, userAbortCallback);
activeRequestBox.Closed += RequestBoxClosedHandler;
activeRequestBox.Show();
};
this.Dispatcher.Invoke(openWindow);
}
// invoked on request box thread
private void RequestBoxClosedHandler(object sender, EventArgs e)
{
activeRequestBox = null;
}
// invoked on domain thread
private void CloseRequestBox()
{
if (activeRequestBox != null)
{
Action closeWindow =
() => activeRequestBox.Close();
this.Dispatcher.Invoke(closeWindow);
}
}
I am creating Selenium RC test scripts in Visual Studio (C#). I am
struggling with re-factoring the tests; all my tests are in a single
file. I would appreciate any input and/or pointers to websites, books,
etc. to learn about modularizing the tests.
I have to run the same tests on different sites (same application but
configured differently for different clients and logins) which are 95%
same. Would anybody like to provide some good examples or best
practices to do this?
Thanks!
Best practise for writing Selenium tests or any UI tests is Page Object Model which is the idea that you create an Object for each of the pages. Each of these objects abstract the page so when you write a test it doesnt really look like you have been working with Selenium.
So for a blog you would do something like this to create an object for the home page
public class Home
{
private readonly ISelenium _selenium;
/// <summary>
/// Instantiates a new Home Page object. Pass in the Selenium object created in the test SetUp().
/// When the object in instantiated it will navigate to the root
/// </summary>
/// <param name="selenium">Selenium Object created in the tests
public Home(ISelenium selenium)
{
this._selenium = selenium;
if (!selenium.GetTitle().Contains("home"))
{
selenium.Open("/");
}
}
/// <summary>
/// Navigates to Selenium Tutorials Page. Selenium object wll be passed through
/// </summary>
/// <returns>SeleniumTutorials representing the selenium_training.htm</returns>
public SeleniumTutorials ClickSelenium()
{
_selenium.Click("link=selenium");
_selenium.WaitForPageToLoad("30000");
return new SeleniumTutorials(_selenium);
}
/// <summary>
/// Click on the blog or blog year and then wait for the page to load
/// </summary>
/// <param name="year">blog or blog year
/// <returns>Object representing /blog.* pages</returns>
public Blog ClickBlogYear(string year)
{
_selenium.Click("link=" + year);
_selenium.WaitForPageToLoad("30000");
return new Blog(_selenium);
}
// Add more methods as you need them
}
then you would create a test that looks like the following
[TestFixture]
public class SiteTests
{
private ISelenium selenium;
[SetUp]
public void Setup()
{
selenium = new DefaultSelenium("localhost", 4444, "*chrome", "http://www.theautomatedtester.co.uk");
selenium.Start();
}
[TearDown]
public void Teardown()
{
selenium.Stop();
}
[Test]
public void ShouldLoadHomeThenGoToXpathTutorial()
{
Home home = new Home(selenium);
SeleniumTutorials seleniumTutorials = home.ClickSelenium();
SeleniumXPathTutorial seleniumXPathTutorial = seleniumTutorials.ClickXpathTutorial();
Assert.True(seleniumXPathTutorial.
IsInputOnScreen(SeleniumXPathTutorial.FirstInput));
Assert.True(seleniumXPathTutorial
.IsInputOnScreen(SeleniumXPathTutorial.SecondInput));
Assert.True(seleniumXPathTutorial
.IsInputOnScreen(SeleniumXPathTutorial.Total));
}
}