Mobile Specific Views / Device Detection - asp.net-core

In the .NET Core docs there is a page titled "Building Mobile Specific Views" but is under construction: https://docs.asp.net/en/latest/mvc/views/mobile.html.
Does anyone have some insight on building mobile views or successfully doing device detection?

Serving specific views based on the browser's user-agent is an outdated concept as it do not sufficiently says much about the capabilities of the device. For example, iPhone and iPad come in different screen sizes and even mobile browsers allow to change the user-agent.
The new concept is called Responsive Design where one creates a single page that fits and show/hides certain element based on the available screen width. One popular responsive deisgn CSS Framework is Bootstrap, originally developed by Twitter and later open-sourced.
Here is an example of responsive design. When you go to the site and change the width of your browser, the design updates as well from 3 to 2 to 1 column design with browser or mobile like navigation (with the Hamburger menu).

This feature actually was not implemented by microsoft. There is couple open discussions for this question:
https://github.com/aspnet/Mvc/issues/4877
https://github.com/aspnet/Razor/issues/751
As a generic answer from them - use responsive web design and css media queries (which from my point of view is not perfect answer for team that claims himself for building general web framework).
There is a implementation for this feature exist as pull request - https://github.com/aspnet/Mvc/pull/4878.
Since this pull request seems to be forgotten, i extract this code into separate project which is available on
https://github.com/laskoviymishka/MvcDeviceDetector.
You may use this implementation (which is easy to add to exist MVC project) or implement this itself. This is pretty easy - you need just implement and reqister own IViewLocationExpander for that.

This can be handle in .Net Core using RazorViewEngineOptions
1) Create Service LocationExpanderService.cs
public class LocationExpanderService : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
//replace the Views to MyViews..
var viewLocationsFinal = new List<string>();
if (!string.IsNullOrEmpty(context.Values["viewCustom"]))
{
foreach (var viewLocation in viewLocations)
{
viewLocationsFinal.Add(viewLocation.Replace(".cshtml", ".mobile.cshtml"));
}
}
viewLocationsFinal.AddRange(viewLocations);
return viewLocationsFinal;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var userAgent = context.ActionContext.HttpContext.Request.Headers["User-Agent"].ToString().ToLower();
var viewCustom = userAgent.Contains("android") || userAgent.Contains("iphone") ? "mobile" : "";
context.Values["viewCustom"] = viewCustom;
}
}
2) Configure services in startup.cs
services.Configure<RazorViewEngineOptions>(o =>
{
o.ViewLocationExpanders.Add(new LocationExpanderService());
});
3) Create view with .mobile extension
Index.mobile.cshtml

Related

ASP.NET Core 6 MVC - access a view (.cshtml) page from a different class library project

We have a scenario where we have to move couple of our view pages away from our Web API project to a separate class library. Where this class library will be be consumed by different Web API projects that needs to load these shared View pages as part of the functionality. I have been looking for a day now but cannot find a way how to do it.
The view pages work with no problems when accessed from within the Web API project but we have now moved these View pages into our existing common library (a class library) and added it as a reference to the Web API project. Basically when we build the application with the common class library containing the views (we changed the property to Content so it gets added on build time), it gets built and copied into the bin folder. So from this, we could say that the view files should be reachable as its just within the project assembly bin folder.
What happens now is that even if setting the web application builder to specify the Content directory to point to this, it still cannot see the View pages and I get an error
The view was not found
Code:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
Args = args,
ContentRootPath = PlatformServices.Default.Application.ApplicationBasePath
});
What bugs me is that the same /Views folder is generated when we put back the View pages back to the Web API project. Same structure and files. Only when put to another project, it now cannot recognize it. Having the view pages on the Web API works while putting it to another project does not.
This is a required structure that we need to implement without the use of a RCL but would still work when referenced by different Web APIs. This may seem odd but this is what we need to do and if possible with only minimal changes.
Your help is very much appreciated!
After a few tries, we we're able to do this by setting the resource object to Embedded Resource and implementing the ManifestEmbeddedFileProvider in the common library to virtually map the location when it gets published as a NuGet. In this case, say like we have a folder named /StaticResources in our common lib. In the sample code below, Program refers to your program assembly or any class object within your application.
Code:
// Get embedded file assembly path to allow our static files to be read by the consuming apps
var manifestEmbeddedProvider = new ManifestEmbeddedFileProvider(
typeof(Program).Assembly,
"/StaticResources");
// Sets the `/StaticResources` folder to be servable like a wwwroot folder
app.UseStaticFiles(new StaticFileOptions {
FileProvider = manifestEmbeddedProvider,
RequestPath = "/Resources"
});
// Use it like this
<script src="/Resources/MyScrtipt.js"></script>
For the View() to work, assuming your views are in /StaticResources folder.
var viewsFileProvider = new ManifestEmbeddedFileProvider(
typeof(Program).Assembly,
"/StaticResources");
app.UseStaticFiles(new StaticFileOptions {
FileProvider = viewsFileProvider,
RequestPath = "/Views/Shared"
});
Hope this helps anyone who comes across this issue. Thank you for all of you who have shared their answers.

Dynamically include precompiled (Razor) views in .NET Core 2.1

Idea
I'm trying to build a modular .NET Core MVC application, were I can add 'modules' (extensions) just by copying the required libraries to a predefined folder. The application defines a set of dependencies (DI), error handling and basic layout; the modules provide the actual application or page logic.
We've build a proof of concept, using ExtCore - the concept works great, Controllers are added automatically and the 'plain' Views are recognized easily (the ones set as 'Embedded Resource').
Challenge
However, I don't like the fact that Views are set to 'Embedded Resource', as it will be a small performance hit as the Razor page is now build at runtime.
What we've tried
As .NET Core 2.1 now has the ability to create Reusable Razor UI, I've created a project with a reference to <Project Sdk="Microsoft.NET.Sdk.Razor"> and compiled it to a Razor library (*.Views.dll).
When I link the Razor View library as project dependency in (main) application, everything works fine. However, the goal still is to make the application fully plug-and-play and allow libraries to be included just by deploying them to a folder.
I've tried to achieve this extending the Razor View Engine using a PhysicalFileProvider, but it won't recognize the views in the library.
services.Configure<RazorViewEngineOptions>(opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider("path/to/extension"));
});
The result always is an exception telling me that /Views/[controller]/[action].cshtml is not found.
Searching other answers I came across this project, which creates a ViewLocationExpander. I tried implementing various versions of this idea without luck - most likely because this is an older concept based on .NET Core 1.x which still uses a project dependency; although the Razor View engine now searches the namespace of the assembly.
services.Configure<RazorViewEngineOptions>(opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider("path/to/extension");
opt.ViewLocationExpanders.Add(new DynamicLocationExpander());
});
Where DynamicLocationExpander looks like:
public class DynamicLocationExpander: IViewLocationExpander
{
public DynamicLocationExpander()
{
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
string assembly = ((ControllerActionDescriptor)context.ActionContext.ActionDescriptor)
.ControllerTypeInfo.AsType().Assembly.GetName().Name;
foreach (var viewLocation in viewLocations)
yield return $"{assembly}/{viewLocation}";
}
}
Question
How can we dynamically link a Razor View library, containing precompiled views, to the Razor Engine in a (parent) application?

UWP - Get Slidervalue from another Page

I've got 2 Pages: My Main-Page and my Settings-Page - both in XAML and xaml.cpp as code behind. I've also got a class where I want to work with the values of the Settings-Class. (As I already asked in another post) I can save the values from the Settings-Page in the cache using the XAML-tag NavigationCacheMode="Enabled". Using this my SliderValue stays the same even when I switch to my Main-Page and back again to Settings-Page.
What I want to do now: Save the value of the slider as soon as it is changed and store this value to a variable to which I have access from a third Class.
What I've got so far (not working, always showing initialvalue '70'):
Settings.xaml.cpp:
UWPApp::Settings::Settings()
{
InitializeComponent();
quality = sldQuality->Value;
}
int Settings::getQuality() {
return quality;
}
void DirectX12XamlApp::Settings::sldQuality_ValueChanged(Platform::Object^ sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs^ e)
{
quality = sldQuality->Value;
}
In Settings.xaml.h:
public:
Settings();
int getQuality();
private:
int quality;
In Settings.xaml:
<Slider x:Name="sldQuality" ValueChanged="sldQuality_ValueChanged" HorizontalAlignment="Left" Margin="38,325,0,0" VerticalAlignment="Top" Width="168" Value="70"/>
//initial-Value = 70, which is ALWAYS showing when using getQuality() in an other Class
In my logicclass I want to use it like this:
int quality = Settings::getQuality();
Quick answer: save the value of the slider in a public static property, ideally in the separate class.
Recommended answer:
You should use MVVM structure in your app, so you would have a separate layer for storing your settings data. You can save the value of the slider in the singleton service, ideally injected through the IoC container. You may also want to save it in the local storage so the settings are saved when the user closes the app.
MVVM is really a standard of XAML-based apps, and the concept is not difficult to learn and implement.
Sounds like you're looking for a way to store and retrieve settings data for your app. Features for that have been built into the UWP. Please refer to this article from the Windows Dev Center. Examples on that page are for C#, but all the links to UWP API references contain code snippets for C++ as well.

How can I use different themes for mobile and main versions Yii?

I have Yii project with main and mobile versions. Views files of mobile version has path
modules/mobile/views/nameController/ . For main version created theme, all views loading from path themes/nameTheme. In config writed 'theme' => 'nameTheme', and in controller I use code:
public function init() {
...
Yii::app()->theme = 'mobile';
...
return parent::init();
}
I moved files of mobile versions to new theme. But Yii loaded views from modules/mobile/views/nameController/. I don't know how define theme for mobile versions in config. Can I use other theme for mobile version in my project (together with theme for main version)?
Thank you in advance.
The way I do my dynamic theme switching, I have method within my Controller component that determines what kind of browser the client uses and then set the theme from within the 'init' method. Very similar to what you've done.
I think the difference is in file organization. If you have separate view files for your desktop and mobile themes, I'd suggest that you place the view files withing the respective theme directories.
I usually make use of a single markup for my themes and just modify the style sheets for both the desktop and mobile themes so I have to worry about it once.
Here's how I do it:
public function isMobileBrowser()
{
$useragent=$_SERVER['HTTP_USER_AGENT'];
return preg_match('/android.+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i',$useragent)||preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|e\-|e\/|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\-|2|g)|yas\-|your|zeto|zte\-/i',substr($useragent,0,4));
}
And this is my Controller::init method :
public function init()
{
if ($this->isMobileBrowser()) {
Yii::app()->theme = "mobile-white-blue";
}
parent::init();
}
If you have multiple view files scattered around your application, Yii looks first for the view files within your theme folder: AppRoot>>Themes>>{theme_name}>>views and if it can't find it there, Yii looks in the primary view folder: AppRoot>>protected>>views or if it's a module view, AppRoot>>protected>>modules>>{module_name}>>views
I hope that's helpful.

MEF + SL4 question

I'm working on an app in the Silverlight 4 RC and i'm taking the oppertunity to learn MEF for handling plugin controls. I've got it working in a pretty basic manor, but it's not exactly tidy and I know there is a better way of importing multiple xap's.
Essentially, in the App.xaml of my host app, I've got the following telling MEF to load my xap's:
AggregateCatalog catalog = new AggregateCatalog();
DeploymentCatalog c1 = new DeploymentCatalog(new Uri("TestPlugInA.xap", UriKind.Relative));
DeploymentCatalog c2 = new DeploymentCatalog(new Uri("TestPlugInB.xap", UriKind.Relative));
catalog.Catalogs.Add(c1);
catalog.Catalogs.Add(c2);
CompositionHost.Initialize(catalog);
c1.DownloadAsync();
c2.DownloadAsync();
I'm sure I'm not using the AggregateCatalog fully here and I need to be able to load any xap's that might be in the directory, not just hardcoding Uri's obviously....
Also, in the MainPage.xaml.cs in the host I have the following collection which MEF puts the plugin's into:
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<IPlugInApp> PlugIns { get; set; }
Again, this works, but I'm pretty sure I'm using ImportMany incorrectly....
Finally, the MainPage.xaml.cs file implements IPartImportsSatisfiedNotification and I have the following for handling the plugin's once loaded:
public void OnImportsSatisfied()
{
sp.Children.Clear();
foreach (IPlugInApp plugIn in PlugIns)
{
if (plugIn != null)
sp.Children.Add(plugIn.GetUserControl());
}
}
This works, but it seems filthy that it runs n times (n being the number of xap's to load). I'm having to call sp.Children.Clear() as if I don't, when loading the 2 plugin's, my stack panel is populated as follows:
TestPlugIn A
TestPlugIn A
TestPlugIn B
I'm clearly missing something here. Can anyone point out what I should be doing?
Thanks!
I think most of what you are doing is fine. Although ObservableCollections do support notifications of individual elements being added and removed, MEF doesn't take advantage of this. In your case it will simply clear the collection and then add all the plugins. Since you are using OnImportsSatisfied for the change notification, you don't even need an ObservableCollection. You could just use an IEnumerable for your import.
To add flexibility in downloading different xaps, I would expose a service in your container that can be imported and that provides the functionality to download a xap given a url. Then any component in your container can trigger a download, and the url to download can come from whatever source you deem appropriate.