Related
I've been fighting with this problem for hours... and I can't find what it is...
I'm just trying to localize the _Layout.cshtml file. Both the IStringLocalizer and the IHtmlLocalizer do not seem to find the Resource files.
I've followed and searched for:
https://github.com/MormonJesus69420/SharedResourcesExample
.Net Core Data Annotations - localization with shared resources
https://stackoverflow.com/search?q=shared+resources+.net+core
https://andrewlock.net/adding-localisation-to-an-asp-net-core-application/
There's something silly that I may be overlooking.
Here's my startup.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using EduPlaTools.Data;
using EduPlaTools.Models;
using EduPlaTools.Services;
using System.Globalization;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Pomelo.EntityFrameworkCore.MySql;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
namespace EduPlaTools
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// This is for string translation!
// Adds Localization Services (StringLocalizer, HtmlLocalizer, etc.)
// the opts.ResourcesPath = is the path in which the resources are found.
// In our case the folder is named Resources!
// There's specific and neutral resources. (Specific en-US). (Neutral: es)
/**
* If no ResourcesPath is specified, the view's resources will be expected to be next to the views.
* If ResourcesPath were set to "resources", then view resources would be expected to be ina Resource directory,
* in a path speicifc to their veiw (Resources/Views/Home/About.en.resx, for example).
*
* */
services.AddLocalization(opts => opts.ResourcesPath = "Resources");
// services.AddBContext
// There are subtle differences between the original and the modified version.
services.AddDbContextPool<ApplicationDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("MySQLConnection"),
mysqlOptions =>
{
mysqlOptions.ServerVersion(new Version(8, 0, 12), ServerType.MySql); // replace with your Server Version and Type
}
));
//options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// This may be dangerous and is not recommended
using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
.Database.Migrate();
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
// These must line up with the ending of the .resx files.
// Example: SharedResources.en.resx, SharedResources.es.rex
// If you want to add specific, then do it like:
// new CultureInfo("en-US")
List<CultureInfo> supportedCultures = new List<CultureInfo>
{
new CultureInfo("es"),
new CultureInfo("en"),
new CultureInfo("es-ES"),
new CultureInfo("en-US")
};
// Registers the localization, and changes the localization per request.
app.UseRequestLocalization(new RequestLocalizationOptions
{
// We give the default support of Spanish.
DefaultRequestCulture = new RequestCulture("es"),
// Format numbers, dates, etc.
SupportedCultures = supportedCultures,
// The strings that we have localized
SupportedUICultures = supportedCultures
});
// This will seed the databse:
SeedDatabase.Initialize(app.ApplicationServices);
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Here's how I'm trying to call it inside the _Layout.cshtml:
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
#inject IStringLocalizer<SharedResources> SharedLocalizer
#inject IHtmlLocalizer<SharedResources> _localizer;
#SharedLocalizer["Menu_Home"]
Here's the directory structure:
Here are the contents of SharedResources.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace EduPlaTools
{
/**
* This is a dummy class that is needed so Localization works.
* Now in .NET Core Localization works as a service, and implementsw
* naming conventions (AT the file level). Therefore, if the files do not
* implement the correct name, there's going to be problems.
*
* See an example, here:
* https://github.com/SteinTheRuler/ASP.NET-Core-Localization/blob/master/Resources/SharedResources.cs
*
* This is a workaround to create a Resource File that can be read by the entire
* application. It's left in blank so the convention over configuration
* picks it up.
*
* */
public class SharedResources
{
}
}
Here are the contents of the resx files:
I've also tried renaming them to no avail.. (Tried Resources.es.rex, Resources.rex)
I tried setting breakpoints to see how it behaved. It of course, didn't find the Resource files. I then compared it with Mormon's repo by recalling an inexistent key. I compared it with my output, but Mormon's repo doesn't display the "SearchedLocation" (Was it introduced in a later .NET Core version?)
Mormon's Repo:
My repo:
I know this may be something silly... But it's been close to 4 hours, and I can't stop since I have a LOT to do!!
Any ideas?
if you want to implement localization with shared resource, you have to create your own culture localizer class:
public class CultureLocalizer
{
private readonly IStringLocalizer _localizer;
public CultureLocalizer(IStringLocalizerFactory factory)
{
var type = typeof(ViewResource);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
_localizer = factory.Create("ViewResource", assemblyName.Name);
}
// if we have formatted string we can provide arguments
// e.g.: #Localizer.Text("Hello {0}", User.Name)
public LocalizedString Text(string key, params string[] arguments)
{
return arguments == null
? _localizer[key]
: _localizer[key, arguments];
}
}
then register it is startup:
services.AddSingleton<CultureLocalizer>();
and modify view locaization settings :
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddViewLocalization(o=>o.ResourcesPath = "Resources")
in your views you have to inject the culture localizer class before using it.
those are initial settings for view localization with shared resource, you need to configure localization settings for DataAnnotation, ModelBinding and Identity error messages as well.
these articles could help for starting:
Developing multicultural web application with ASP.NET Core 2.1 Razor Pages:
http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application
it includes step by step tutorial for localizing using shared resources, additionally, this article is about localizing Identity error messages :
http://ziyad.info/en/articles/20-Localizing_Identity_Error_Messages
I wanted to add an answer which further develops Laz's solution. Just in case someone wants to have individual localized views.
Back in Startup.cs, you have:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddViewLocalization(o=>o.ResourcesPath = "Resources")
Technically, you are indicating MVC to look in the "Resources" folder as the main path, and then follow the convention to look for localized resource files.
Therefore
In case you want to localize the Login.cshtml view found in Views/Account/Login.chsmtl, you have to create the resource file in: Resources/Views/Account/Login.en.resx
You would then need to add the following either in the view directly Login.cshtml or in the _ViewImports.cshtml to reference it to all the views:
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
After that, in your code you can do:
Localizer["My_Resource_file_key"]
And you'll have it translated.
Here are some illustrations:
An update to the previous answers. Due to the recent breaking change in .NET Core 3 (https://github.com/dotnet/docs/issues/16964), the accepted answer will only work if the resource lives directly in the resource folder.
I have created a workaround to use shared resources in views (same applies to controllers, data annotations, services, whatever you need...).
First you need to create an empty class for your resources. This one has to live under YourApp.Resources namespace. then create your resources named same as your class (in my example I have Views.cs in the namespace MyApp.Resources.Shared and Views.resx).
Then here is the helper class to load the shared resources:
public class SharedViewLocalizer
{
private readonly IStringLocalizer _localizer;
public SharedViewLocalizer(IStringLocalizerFactory factory)
{
var assemblyName = new AssemblyName(typeof(Resources.Shared.Views).GetTypeInfo().Assembly.FullName);
localizer = factory.Create("Shared.Views", assemblyName.Name);
}
public string this[string key] => _localizer[key];
public string this[string key, params object[] arguments] => _localizer[key, arguments];
}
You have to register is in the Startup.Configure:
services.AddSingleton<SharedViewLocalizer>();
I suppose you use
services.AddLocalization(options => options.ResourcesPath = "Resources");
to setup default resources location.
And then in your view you use it as follows:
#inject IViewLocalizer _localizer
#inject SharedViewLocalizer _sharedLocalizer
#_localizer["View spacific resource"] // Resource from Resources/Views/ControllerName/ViewName.resx
#_sharedLocalizer["Shared resource"] // Resource from Resources/Shared/Views.resx
#_sharedLocalizer["Also supports {0} number of arguments", "unlimited"]
Same principle can be applied to DataAnnotations where we can use the built-in method in Startup.Configure:
services.AddMvc()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
var assemblyName = new AssemblyName(typeof(DataAnnotations).GetTypeInfo().Assembly.FullName);
return factory.Create("Shared.DataAnnotations", assemblyName.Name
};
})
.AddViewLocalization();
Again, I'm expecting my resources to live in the namespace Resources.Shared and have an empty class called DataAnnotations created.
Hope this helps to overcome the current breaking change problems.
I want to make a Xamarin.Forms project, targeting iOS, Android and Windows Phone.
My app needs to authenticate users using Facebook.
Should I implement login for each platform independently, or use a manual flow?
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.0
I prefer to have a single implementation of the login flow, and use it on all platforms.
How can I get a single implementaion of the Facebook login flow?
UPDATE (10/24/17): While this way of doing things was okay a few years ago, I now strongly advocate for using native UI for doing authentication, as opposed to the webview method shown here. Auth0 is a great way to accomplish native UI login for your apps, using a wide variety of identity providers:
https://auth0.com/docs/quickstart/native/xamarin
EDIT: I finally put a sample for this on Gihub
I posted an answer on the Xamarin Forums. I'll repeat it here.
Let's start with the core of the app, the Xamarin.Forms PCL project. Your App class will look something like this:
namespace OAuth2Demo.XForms
{
public class App
{
static NavigationPage _NavPage;
public static Page GetMainPage ()
{
var profilePage = new ProfilePage();
_NavPage = new NavigationPage(profilePage);
return _NavPage;
}
public static bool IsLoggedIn {
get { return !string.IsNullOrWhiteSpace(_Token); }
}
static string _Token;
public static string Token {
get { return _Token; }
}
public static void SaveToken(string token)
{
_Token = token;
}
public static Action SuccessfulLoginAction
{
get {
return new Action (() => {
_NavPage.Navigation.PopModalAsync();
});
}
}
}
}
The first thing to notice is the GetMainPage() method. This tells the app which screen it should load first upon launching.
We also have a simple property and method for storing the Token that is returned from the auth service, as well as a simple IsLoggedIn property.
There's an Action property as well; something I stuck in here in order to have a way for the platform implementations to perform a Xamarin.Forms navigation action. More on this later.
You'll also notice some red in your IDE because we haven't created the ProfilePage class yet. So, let's do that.
Create a very simple ProfilePage class in the Xamarin.Forms PCL project. We're not even going to do anything fancy with it because that will depend on your particular need. For the sake of simplicity in this sample, it will contain a single label:
namespace OAuth2Demo.XForms
{
public class ProfilePage : BaseContentPage
{
public ProfilePage ()
{
Content = new Label () {
Text = "Profile Page",
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.CenterAndExpand,
};
}
}
}
Again, you'll probably have some red in your IDE because we seem to be missing the BaseContentPage class. The sole purpose of the BaseContentPage class is to ensure that none of the app's screens can be displayed until the user has logged in. (In this simplified demo, we're just persisting the user info to memory, so you'll need to re-login every time the app is run. In a real-world app, you'd be storing the authenticated user info to the device's keychain, which would eliminate the need to login at each app start.)
Create a BaseContentPage class in the Xamarin.Forms PCL project:
namespace OAuth2Demo.XForms
{
public class BaseContentPage : ContentPage
{
protected override void OnAppearing ()
{
base.OnAppearing ();
if (!App.IsLoggedIn) {
Navigation.PushModalAsync(new LoginPage());
}
}
}
}
There's a few interesting things going on here:
We're overriding the OnAppearing() method, which is similar to the ViewWillAppear method in an iOS UIViewController. You can execute any code here that you'd like to have run immediately before the screen appears.
The only thing we're doing in this method is checking to see if the user is logged in. If they're not, then we perform a modal push to a class called LoginPage. If you're unfamiliar with the concept of a modal, it's simply a view that takes the user out of the normal application flow in order to perform some special task; in our case, to perform a login.
So, let's create the LoginPage class in the Xamarin.Forms PCL project:
namespace OAuth2Demo.XForms
{
public class LoginPage : ContentPage
{
}
}
Wait...why doesn't this class have a body???
Since we're using the Xamatin.Auth component (which does the job of building and presenting a web view that works with the provided OAuth2 info), we actually don't want any kind of implementation in our LoginPage class. I know that seems weird, but bear with me.
The LoginPageRenderer for iOS
Up until this point, we've been working solely in the Xamarin.Forms PCL project. But now we need to provide the platform-specific implementation of our LoginPage in the iOS project. This is where the concept of a Renderer comes in.
In Xamarin.Forms, when you want to provide platform-specific screens and controls (i.e. screens that do not derive their content from the abstract pages in the Xamarin.Forms PCL project), you do so with Renderers.
Create a LoginPageRenderer class in your iOS platform project:
[assembly: ExportRenderer (typeof (LoginPage), typeof (LoginPageRenderer))]
namespace OAuth2Demo.XForms.iOS
{
public class LoginPageRenderer : PageRenderer
{
public override void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
var auth = new OAuth2Authenticator (
clientId: "", // your OAuth2 client id
scope: "", // the scopes for the particular API you're accessing, delimited by "+" symbols
authorizeUrl: new Uri (""), // the auth URL for the service
redirectUrl: new Uri ("")); // the redirect URL for the service
auth.Completed += (sender, eventArgs) => {
// We presented the UI, so it's up to us to dimiss it on iOS.
App.SuccessfulLoginAction.Invoke();
if (eventArgs.IsAuthenticated) {
// Use eventArgs.Account to do wonderful things
App.SaveToken(eventArgs.Account.Properties["access_token"]);
} else {
// The user cancelled
}
};
PresentViewController (auth.GetUI (), true, null);
}
}
}
}
There are important things to note:
The [assembly: ExportRenderer (typeof (LoginPage), typeof (LoginPageRenderer))] line at the top (and importantly before the namespace declaration) is using the Xamarin.Forms DependencyService. It's not the most beautiful thing in the world because it's not IoC/DI, but whatever...it works. This is the mechanism that "maps" our LoginPageRenderer to the LoginPage.
This is the class in which we're actually using the Xamarin.Auth component. That's where the OAuth2Authenticator reference comes from.
Once the login is successful, we fire off a Xamarin.Forms navigation via App.SuccessfulLoginAction.Invoke();. This gets us back to the ProfilePage.
Since we're on iOS, we're doing all of our logic sinde of the ViewDidAppear() method.
The LoginPageRenderer for Android
Create a LoginPageRenderer class in your Android platform project. (Note that class name you're creating is identical to the one in the iOS project, but here in the Android project the PageRenderer inherits from Android classes instead of iOS classes.)
[assembly: ExportRenderer (typeof (LoginPage), typeof (LoginPageRenderer))]
namespace OAuth2Demo.XForms.Android
{
public class LoginPageRenderer : PageRenderer
{
protected override void OnModelChanged (VisualElement oldModel, VisualElement newModel)
{
base.OnModelChanged (oldModel, newModel);
// this is a ViewGroup - so should be able to load an AXML file and FindView<>
var activity = this.Context as Activity;
var auth = new OAuth2Authenticator (
clientId: "", // your OAuth2 client id
scope: "", // the scopes for the particular API you're accessing, delimited by "+" symbols
authorizeUrl: new Uri (""), // the auth URL for the service
redirectUrl: new Uri ("")); // the redirect URL for the service
auth.Completed += (sender, eventArgs) => {
if (eventArgs.IsAuthenticated) {
App.SuccessfulLoginAction.Invoke();
// Use eventArgs.Account to do wonderful things
App.SaveToken(eventArgs.Account.Properties["access_token"]);
} else {
// The user cancelled
}
};
activity.StartActivity (auth.GetUI(activity));
}
}
}
Again, let's take a look at some interesting things:
The [assembly: ExportRenderer (typeof (LoginPage), typeof (LoginPageRenderer))] line at the top (and importantly before the namespace declaration) is using the Xamarin.Forms DependencyService. No difference here from the iOS version of LoginPageRenderer.
Again, this is where we're actually using the Xamarin.Auth component. That's where the OAuth2Authenticator reference comes from.
Just as with the iOS version, once the login is successful, we fire off a Xamarin.Forms navigation via App.SuccessfulLoginAction.Invoke();. This gets us back to the ProfilePage.
Unlike the iOS version, we're doing all of the logic inside of the OnModelChanged() method instead of the ViewDidAppear().
Here it is on iOS:
...and Android:
UPDATE:
I've also provided a detailed sample at my blog: http://www.joesauve.com/using-xamarin-auth-with-xamarin-forms/
You could consume either Xamarin.Social or Xamarin.Auth for that. It allows using the same api whatever the platform is.
As of now, those libs aren't PCL yet, but you still can consume them from a Shared Assets Project, or abstract the API you need in an interface and inject in with DependencyService or any other DI container.
I've created a sample project to show how to create a Facebook login using native Facebook component, not through a webview like the solutions suggested here.
You can check it out in this address:
https://github.com/IdoTene/XamarinFormsNativeFacebook
IOS 8: For those who are using #NovaJoe code and are stuck on view, add the code bellow to workaround:
bool hasShown;
public override void ViewDidAppear(bool animated)
{
if (!hasShown)
{
hasShown = true;
// the rest of #novaJoe code
}
}
Here's a good Xamarin.Forms authentication sample. The documentation in the code is nice. It uses a webview to render the login screen, but you can select what login type you want. It also saves a users token so he doesn't have to keep re-logging in.
https://github.com/rlingineni/Xamarin.Forms_Authentication
Another addition to #NovaJoe's code, on iOS8 with Facebook, you'd need to modify the Renderer class as below to close the View after successful authentication.
auth.Completed += (sender, eventArgs) => {
// We presented the UI, so it's up to us to dimiss it on iOS.
/*Importand to add this line */
DismissViewController (true, null);
/* */
if (eventArgs.IsAuthenticated) {
App.Instance.SuccessfulLoginAction.Invoke ();
// Use eventArgs.Account to do wonderful things
App.Instance.SaveToken (eventArgs.Account.Properties ["access_token"]);
} else {
// The user cancelled
}
};
The correct implementation for the Androids PageRenderer is:
using System;
using Android.App;
using Android.Content;
using OAuth2Demo.XForms.Android;
using Xamarin.Auth;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using XamarinAuth;
[assembly: ExportRenderer(typeof(LoginPage), typeof(LoginPageRenderer))]
namespace OAuth2Demo.XForms.Android
{
public class LoginPageRenderer : PageRenderer
{
public LoginPageRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
// this is a ViewGroup - so should be able to load an AXML file and FindView<>
var activity = this.Context as Activity;
var auth = new OAuth2Authenticator(
clientId: "<Constants.clientId>", // your OAuth2 client id
scope: "<Constants.scope>", // the scopes for the particular API you're accessing, delimited by "+" symbols
authorizeUrl: new Uri("<Constants.authorizeUrl>"), // the auth URL for the service
redirectUrl: new Uri("<Constants.redirectUrl>")); // the redirect URL for the service
auth.Completed += (sender, eventArgs) =>
{
if (eventArgs.IsAuthenticated)
{
App.SuccessfulLoginAction.Invoke();
// Use eventArgs.Account to do wonderful things
App.SaveToken(eventArgs.Account.Properties["access_token"]);
}
else
{
// The user cancelled
}
};
activity.StartActivity(auth.GetUI(activity));
}
}
}
I have two asp.net MVC 3 web projects that uses the same views, and i would like to put those views in a web project that contains common items like controllers, actionfilters, Extension methods, etc... (the project folder just above those two projects).
I created this custom view engine modifying the views and partial view, locations.
public class GenericViewEngine : RazorViewEngine
{
public GenericViewEngine(): this(null)
{ }
public GenericViewEngine(IViewPageActivator viewPageActivator)
{
base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/../Framework.Web/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", "~/../Framework.Web/Views/Shared/{0}.cshtml" };
base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/../Framework.Web/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", "~/../Framework.Web/Views/Shared/{0}.cshtml" };
base.PartialViewLocationFormats = new string[] { "~/Views/{1}/{0}.cshtml", "~/../Framework.Web/Views/{1}/{0}.cshtml", "~/Views/Shared/{0}.cshtml", "~/../Framework.Web/Views/Shared/{0}.cshtml" };
base.FileExtensions = new string[] { "cshtml" };
}
protected override IView CreatePartialView (ControllerContext controllerContext, string partialPath)
{
return base.CreatePartialView(controllerContext, partialPath);
}
protected override IView CreateView (ControllerContext controllerContext, string viewPath, string masterPath)
{
return base.CreateView(controllerContext, viewPath, masterPath);
}
}
Im getting an exception that says: Cannot use a leading .. to exit above the top directory.
How i can put the common views of these two projects out of each web projects? Is there any solution for avoid compile the views into the main common web project?
Thanks in advance.
Jose.
Is there any solution for avoid compile the views into the main common web project?
Yes, you could embed them as resources into some plugin assembly and reuse them in multiple projects. For this you will have to write a custom VirtualPathProvider that is able to load views from non-standard locations outside of your current application. You could also take a look at the RazorGenerator project. Here's an accompanying blog post explaining some of the required steps. Basically you need to install the Razor Generator Extension which will precompile your Razor views as part of a class library that you could reference in your MVC application.
I have been using MvvmCross on a cross platform mobile project and have 2 different views in a MonoTouch project that are using the same shared viewmodel and not sure how to go about structuring my code to navigate to different views using the same viewmodel in MvvmCross.
The default convention used by the MvvmCross platform is to automatically register all views using reflection.
This is done in the base Setup class - in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Platform/MvxBaseSetup.cs:
protected virtual void InitializeViews()
{
var container = this.GetService<IMvxViewsContainer>();
foreach (var pair in GetViewModelViewLookup())
{
Add(container, pair.Key, pair.Value);
}
}
where GetViewModelViewLookup returns a dictionary of ViewModel type to View type:
protected virtual IDictionary<Type, Type> GetViewModelViewLookup(Assembly assembly, Type expectedInterfaceType)
{
var views = from type in assembly.GetTypes()
let viewModelType = GetViewModelTypeMappingIfPresent(type, expectedInterfaceType)
where viewModelType != null
select new { type, viewModelType };
return views.ToDictionary(x => x.viewModelType, x => x.type);
}
In universal iPad/iPhone apps you do occasionally want to include multiple views for each viewmodel - using one view in the iPad and one view in the iPhone.
To do this, there are now (literally just now!) some attributes available to mark your views as being "unconventional" - these are:
MvxUnconventionalViewAttribute
use this to mark that your view should never be included by convention
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Views/Attributes/MvxUnconventionalViewAttribute.cs
MvxConditionalConventionalViewAttribute
an abstract attribute - override this to provide your own custom logic for inclusion/exclusion
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Views/Attributes/MvxConditionalConventionalViewAttribute.cs
MvxFormFactorSpecificViewAttribute
iOS/Touch only
an attribute that will include the view if and only if the detected iPhone form factor matches the current device
in https://github.com/slodge/MvvmCross/blob/master/Cirrious/Cirrious.MvvmCross/Touch/Views/Attributes/MvxFormFactorSpecificViewAttribute.cs
The last of these is probably what you want in this case - you could implement simple iPhone/iPad switching for a MainViewModel using two views declared like:
[MvxFormFactorSpecificView(MvxTouchFormFactor.Phone)]
public class MyIPhoneView : BaseView<MainViewModel>
{
// iphone specific view ...
}
[MvxFormFactorSpecificView(MvxTouchFormFactor.Pad)]
public class MyIPadView : BaseView<MainViewModel>
{
// ipad specific view ...
}
Alternatively if you want a very custom configuration, you can override all 'convention-based' behaviour - you can implement your own override of GetViewModelViewLookup - e.g.:
protected override IDictionary<Type, Type> GetViewModelViewLookup(Assembly assembly, Type expectedInterfaceType)
{
if (IsIPad)
{
return new Dictionary<Type, Type>()
{
{ typeof(HomeViewModel), typeof(IPadHomeView) },
{ typeof(DetailViewModel), typeof(IPadDetailView) },
{ typeof(AboutViewModel), typeof(SharedAboutView) },
};
}
else
{
return new Dictionary<Type, Type>()
{
{ typeof(HomeViewModel), typeof(IPhoneHomeView) },
{ typeof(DetailViewModel), typeof(IPhoneDetailView) },
{ typeof(AboutViewModel), typeof(SharedAboutView) },
};
}
}
Note that eventually you may decide that you need additional ViewModels as well as Views for the iPad app - the iPad has, after all, a much bigger screen - in this case you can add them manually. Ultimately, when your app hits a few million users, you may even decide to completely branch the tablet code away from the phone code - but that can generally wait until you hit that few million mark...
Another way to do it is to go ahead and create 2 ViewModels, but have them both subclass an abstract ViewModel, as follows:
FirstViewViewModel : BaseViewModel
SecondViewViewModel : BaseViewModel
With the corresponding views named:
FirstView.xaml
SecondView.xaml
This way, you are able to place some shared behavior in BaseViewModel, while the 2 subclasses are really just there to satisfy MvvmCross' view fetching conventions.
I recently started with MvvmCross and I am using v4.2.1. It seems that some names have changed. I am using one ViewModel with seperate iPhone and iPad views with the following:
[MvxFormFactorSpecific(MvxIosFormFactor.Phone)]
public class MyIPhoneView : BaseView<MainViewModel>
{
// iphone specific view ...
}
[MvxFormFactorSpecific(MvxIosFormFactor.TallPhone)]
public class MyTallIPhoneView : BaseView<MainViewModel>
{
// tall iphone specific view ...
}
[MvxFormFactorSpecific(MvxIosFormFactor.Pad)]
public class MyIPadView : BaseView<MainViewModel>
{
// ipad specific view ...
}
I would like to break out my controllers and views into separate class libraries so they can be reused in multiple ASP.NET MVC 3 applications. The controllers part was not an issue when using a separate assembly, however getting the view engine to locate the view was.
I ended up using Compile your asp.net mvc Razor views into a seperate dll.
Is there an easier way that I missed?
I have modified the idea posted here, to work with MVC3. It was pretty fast and easy. The only minor drawback is that shared views need to be embedded resources, and therefore, compiled.
Put your shared views (.cshtml, .vbhtml files) into a library project. (I also have some shared controllers in this project.) If you want to use the _Layout.cshtml from your application, make sure you include a _ViewStart.cshtml, that points to it, in with your shared views.
In the library project, set all of your views' Build Action properties to Embedded Resource.
In the library project add the following code which will write the contents of your views to a tmp/Views directory.
.
public class EmbeddedResourceViewEngine : RazorViewEngine
{
public EmbeddedResourceViewEngine()
{
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml",
"~/tmp/Views/{0}.cshtml",
"~/tmp/Views/{0}.vbhtml"
};
PartialViewLocationFormats = ViewLocationFormats;
DumpOutViews();
}
private static void DumpOutViews()
{
IEnumerable<string> resources = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceNames().Where(name => name.EndsWith(".cshtml"));
foreach (string res in resources) { DumpOutView(res); }
}
private static void DumpOutView(string res)
{
string rootPath = HttpContext.Current.Server.MapPath("~/tmp/Views/");
if (!Directory.Exists(rootPath))
{
Directory.CreateDirectory(rootPath);
}
Stream resStream = typeof(EmbeddedResourceViewEngine).Assembly.GetManifestResourceStream(res);
int lastSeparatorIdx = res.LastIndexOf('.');
string extension = res.Substring(lastSeparatorIdx + 1);
res = res.Substring(0, lastSeparatorIdx);
lastSeparatorIdx = res.LastIndexOf('.');
string fileName = res.Substring(lastSeparatorIdx + 1);
Util.SaveStreamToFile(rootPath + fileName + "." + extension, resStream);
}
}
I'm using Adrian's StreamToFile writer, found here.
In the Global.asax.cs of your application add:
.
public static void RegisterCustomViewEngines(ViewEngineCollection viewEngines)
{
//viewEngines.Clear(); //This seemed like a bad idea to me.
viewEngines.Add(new EmbeddedResourceViewEngine());
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
RegisterCustomViewEngines(ViewEngines.Engines);
}
Take a look at mvc contrib's portable areas:
http://www.lostechies.com/blogs/hex/archive/2009/11/01/asp-net-mvc-portable-areas-via-mvccontrib.aspx
They were made specifically for this purpose. If you go that road, it is less code you have to mantain ;-)
Just a few additions to Carson Herrick's excellent post...
You will need to resolve a few of the references (you will need to include System.Runtime.Remoting into your project).
Utils.SaveStreamToFile needs to be changed to ->
System.Runtime.Remoting.MetadataServices.MetaData.SaveStreamToFile(resStream, rootPath + fileName + "." + extension);
You may get the error - The view must derive from WebViewPage, or WebViewPage<TModel>. The answer is here: The view must derive from WebViewPage, or WebViewPage<TModel>
When you deploy the project, it is highly likely you will get an error when you load the project. You need to give the APP POOL you are using (full) rights to the folder.