Viewmodel's ObservableCollection and ObservableProperty is throwing a null reference exception - maui-community-toolkit

.net 7
CommunityToolKit.mvvm 8.1.0
I've got many pages working great in a .net maui application. In working on wiring another new page, I've encountered an odd issue. My viewmodel appears to be the same as the others that I have which are working.
First the data object:
public class allRegsGroupped
{
public Guid? visualId { get; set; }
public Guid? eventId { get; set; }
public string? groupName { get; set; }
}
In the view model I have this:
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
using SharedModels;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace App.ViewModels
{
public partial class ManageEventViewModel : ObservableObject, IQueryAttributable
{
[ObservableProperty]
ObservableCollection<allRegsGroupped> allGroupRegs;
[ObservableProperty]
allRegsGroupped test;
EventPublicID _selectedEvent;
public ManageEventViewModel() { }
public void ApplyQueryAttributes(IDictionary<string, object> query)
{
_selectedEvent = query["selectedEvent"] as EventPublicID;
}
public async Task LoadAsync()
{
App.Globals.SetHttpClient();
try
{
EventPublicID epi = _selectedEvent;
var json = JsonConvert.SerializeObject(epi, Formatting.Indented);
var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
var response = await App.Globals.MyHttpClient.PostAsync(App.Globals.APIURL + "getAllRegistrationsForEvent", content);
var allGroupsResponse = response.Content.ReadAsStringAsync().Result;
if (allGroupsResponse != null)
{
List<allRegsGroupped> listOfGroups = JsonConvert.DeserializeObject<List<allRegsGroupped>>(allGroupsResponse);
if (listOfGroups != null)
{
AllGroupRegs = new ObservableCollection<allRegsGroupped>(listOfGroups.OrderBy(x => x.groupName));
}
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message + "\r\b" + ex.StackTrace);
}
}
...
My XAML looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="App.Pages.ManageEventPage"
Title="Manage Event"
xmlns:viewmodel="clr-namespace:App.ViewModels"
xmlns:dm="clr-namespace:SharedModels;assembly=SharedModels"
x:DataType="viewmodel:ManageEventViewModel"
NavigatedTo="ContentPage_NavigatedTo"
>
<VerticalStackLayout>
<CollectionView ItemsSource="{Binding AllGroupRegs}">
<CollectionView.ItemTemplate>
<DataTemplate>
<VerticalStackLayout>
<Label Text="{Binding groupName}" FontAttributes="Bold" />
</VerticalStackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
In the XAML page, if I leave this line in:
<Label Text="{Binding groupName}" FontAttributes="Bold" />
During compile I get:
XFC0045 Binding: Property "groupName" not found on "App.ViewModels.ManageEventViewModel".
groupName is there...
If I remove it, I got an even a weirder behavior.
Originally, I was getting a null reference error when the LoadAsync ran on this line:
AllGroupRegs = new ObservableCollection<allRegsGroupped>(listOfGroups.OrderBy(x => x.groupName));
Even though the data did come down and was in the "listOfGroups". As an attempt to fix it, I re-created the page and viewmodel. After I did that, this section of code started to work. The viewmodel was copied from the old one to the new one so it was the same except for the viewmodel declaration.
Another odd thing I've been experiencing. Occasionally (but somewhat frequently), a viewmodel will lose its mind and throw a whole bunch of compile errors on observable properties which are no longer able to be resolved. It always happens to a viewmodel which is open during run or compile, never to a viewmodel which is not opened. This is fixed by exiting VS2022 and going back into it. I am beginning to wonder if I'm hitting a bug of some sort in CommunityToolkit.

For the compile time errors a work around is to frequently delete the content of obj and bin folders and reload the project. Most of the time you need to exit VS in order to delete the files as they get locked if Visual Studio is open.
As for [OBSERVABLEPROPERTY] I still have code that works and compiles but visual studio gives error on in error list. It comes and goes depending on who know what. But deleting contents of above folders and restarting visual studio fixes a ton of issues.

Humble pie...
The issue was I didn't have the datamodel specified in the datatemplate in the XAML file.
So this:
<DataTemplate>
to this:
<DataTemplate x:DataType="dm:allRegsGroupped">
The other errors persist, but I'm betting they are due to a bug in the framework.

Related

How to present data with binding and DataTemplate -or- ContentControl for MAUI

How to present a string, number or a also view model with binding and DataTemplate?
I am looking for a MAUI replacement for the WPF ContentControl.
The ContentView has a Content property but this is from type View.
The ContentPresenter has a Content property but this is also from type View. <Ignorable>WTF, Why this is not named ViewPresenter when it can only present a View??? Someoteimes MAUI is weird.</Ignorable>
How to present any content with defining DataTemplates for each data type?
class PropertyViewModel {
public string Name {get;set;}
public object Value {get;set;}
}
<Page.Resources>
<DataTemplate DataType="System.String">
<Entry Text="{Binding}/>
</DataTemplate>
<DataTemplate DataType="System.Int32">
<NumberPicker Value="{Binding}/>
</DataTemplate>
.. more templates, eg. DatePicker for System.DateOnly
</Page.Resources>
<DockLayout>
<Label Text="{Binding Name}
<TemplatedContenView Content={Binding Value}/>
</DockPanel>
The TemplatedContenView or ContentControl (that does not exist in MAUI), can use different templates for different types of Value. In WPF the ContentControl uses ContentTemplate, ContentTemplateSelector or if none specified it looked into the resources to find the template.
<Ignorable>I often have the feeling with MAUI that I have to constantly reinvent things that are standard in WPF. Yes I know MAUI is not WPF, but there should still be at least similar concepts. The switch from WinForms to WPF was much easier and the differences were considerably greater.</Ignorable>
Edit1: a more detailed example
I'm a WPF developer and recently I've started MAUI project. And It looks like you have to reinvent the wheel every time when you are going to write such a simple scenario as you mentioned :(. When you do it using WPF you even don't need to thought about that, it's too easy to implement, but when you use MAUI you should break your mind to do such minor things.
I also encountered the same issue and I didn't find a simple in-box solution. But I came up with the idea to create a control with some layout inside that has attached properties from BindableLayout
TemplatedContentPresenter.xaml.cs:
public partial class TemplatedContentPresenter : ContentView
{
public TemplatedContentPresenter()
{
InitializeComponent();
}
public static readonly BindableProperty DataTemplateSelectorProperty = BindableProperty.Create(nameof(DataTemplateSelector), typeof(DataTemplateSelector), typeof(TemplatedContentPresenter), null, propertyChanged: DataTemplateSelectorChanged);
public static readonly BindableProperty DataTemplateProperty = BindableProperty.Create(nameof(DataTemplate), typeof(DataTemplate), typeof(TemplatedContentPresenter), null, propertyChanged: DataTemplateChanged);
public static readonly BindableProperty DataProperty = BindableProperty.Create(nameof(Data), typeof(object), typeof(TemplatedContentPresenter), null, propertyChanged: DataChanged);
public DataTemplateSelector DataTemplateSelector
{
get =>(DataTemplateSelector)GetValue(DataTemplateSelectorProperty);
set => SetValue(DataTemplateSelectorProperty, value);
}
public DataTemplate DataTemplate
{
get => (DataTemplate)GetValue(DataTemplateProperty);
set => SetValue(DataTemplateProperty, value);
}
public object Data
{
get => GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
private static void DataTemplateSelectorChanged(BindableObject bindable, object oldValue, object newValue)
{
if(bindable is TemplatedContentPresenter contentPresenter && newValue is DataTemplateSelector dataTemplateSelector)
{
BindableLayout.SetItemTemplateSelector(contentPresenter.HostGrid, dataTemplateSelector);
}
}
private static void DataTemplateChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is TemplatedContentPresenter contentPresenter && newValue is DataTemplate dataTemplate)
{
BindableLayout.SetItemTemplate(contentPresenter.HostGrid, dataTemplate);
}
}
private static void DataChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is TemplatedContentPresenter contentPresenter)
{
BindableLayout.SetItemsSource(contentPresenter.HostGrid, new object[] { newValue });
}
}
}
TemplatedContentPresenter.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.TemplatedContentPresenter">
<Grid x:Name="HostGrid" x:FieldModifier="private" />
</ContentView>
Usage:
<Frame WidthRequest="500" HeightRequest="500">
<controls:TemplatedContentPresenter
Data="{Binding}"
DataTemplateSelector="{StaticResource CardTemplateSelector}"/>
</Frame>
UPD:
While I was writing the answer I came up with another solution with a simple converter:
SingleObjectToArray.xaml
internal class SingleObjectToArray : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return new object[] { value };
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Usage:
<Frame>
<Frame.Resources>
<converters:SingleObjectToArray x:Key="SingleObjectToArrayConverter"/>
</Frame.Resources>
<Grid BindableLayout.ItemsSource="{Binding Converter={StaticResource SingleObjectToArrayConverter}}"
BindableLayout.ItemTemplateSelector="{StaticResource CardTemplateSelector}" />
</Frame>

How can I use Razor model binding from a resource string?

Normally we have:
<div>
<div>Some property from a model: #Model.Property</div>
</div>
But let's say I have this exact line as a complete html line in a resource file and try to reference it:
<div>
#Resource.MyResourceLine
</div>
The model binding doesn't work. It renders the line raw without binding.
How can I make Razor bind in this scenario?
EDIT:
There's an alternative way that works by changing the content on the resource string to a string.Format placeholder:
<div>Some property from a model:{0}</div>
and then:
<div>
#string.Format(#Resource.MyResourceLine,#Model.Property)
</div>
But that makes it difficult to maintain large texts with many property references. It would be ideal if the property names could be seen in the resource file. Is there a more elegant way?
I did a bit of digging here and there in the source code for Asp.Net Mvc (latest version which is 5.2.3) taken from official codeplex: https://aspnetwebstack.codeplex.com/
Short answer:
There is no easy way for that out of the box, since the page is already compiled, and any string that you pass in your model is treated like a string - either MvcHtmlString or String. You may use RazorEngine package to do it fast and without lots of issues: (https://github.com/Antaris/RazorEngine)
Long answer:
When you open the route, and controller serves the view for it, you have to take parsed and compiled code for that view(which might get generated during startup, or lazily right before you actually use that view) and then render the page combining compiled View and your Model data (which is done when you call View() method in controller).
How ASP.NET parses and compiles the view, generating running code for it:
// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Razor/RazorTemplateEngine.cs
// Line 152
protected internal virtual GeneratorResults GenerateCodeCore(ITextDocument input, string className, string rootNamespace, string sourceFileName, CancellationToken? cancelToken) {
//...
// Run the parser
RazorParser parser = CreateParser();
Debug.Assert(parser != null);
ParserResults results = parser.Parse(input);
// Generate code
RazorCodeGenerator generator = CreateCodeGenerator(className, rootNamespace, sourceFileName);
generator.DesignTimeMode = Host.DesignTimeMode;
generator.Visit(results);
//...
}
How asp.net renders the page, combining source code for view and data from the model
// https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.WebPages/WebPageBase.cs
// Line 215
// public override void ExecutePageHierarchy() {
// ...
try
{
// Execute the developer-written code of the WebPage
Execute(); //**you can see example of the code it executes right below in the code block**
}
finally
{
TemplateStack.Pop(Context);
}
}
After the view is compiled, it's turned into a simple C# class that generates a string, that is then displayed to the user in browser. Let's create a simple Controller, View and ViewModel:
Here's some code:
ViewModel class:
namespace StackOverflow.Models
{
public class TestViewModel
{
public int IntProperty { get; set; }
public string StringProperty { get; set; }
}
}
Controller code:
public ActionResult Test()
{
var viewModel = new TestViewModel
{
IntProperty = 5,
StringProperty = "#DateTime.UtcNow.ToString()"
};
return View(viewModel);
}
View:
#model StackOverflow.Models.TestViewModel
#{
ViewBag.Title = "just a test";
Layout = null;
}
#Model.IntProperty
#Html.Raw(#Model.StringProperty)
An example of the page, using the code above, generates the following compiled view:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ASP {
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Helpers;
using System.Web.Security;
using System.Web.UI;
using System.Web.WebPages;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;
using System.Web.Mvc.Html;
using System.Web.Optimization;
using System.Web.Routing;
using StackOverflow;
public class _Page_Views_Test_Index_cshtml : System.Web.Mvc.WebViewPage<StackOverflow.Models.TestViewModel> {
#line hidden
public _Page_Views_Test_Index_cshtml() {
}
protected ASP.global_asax ApplicationInstance {
get {
return ((ASP.global_asax)(Context.ApplicationInstance));
}
}
public override void Execute() {
#line 3 "XXX\Views\Test\Index.cshtml"
ViewBag.Title = "just a test";
Layout = null;
#line default
#line hidden
BeginContext("~/Views/Test/Index.cshtml", 100, 4, true);
WriteLiteral("\r\n\r\n");
EndContext("~/Views/Test/Index.cshtml", 100, 4, true);
BeginContext("~/Views/Test/Index.cshtml", 105, 17, false);
#line 8 "XXX\Views\Test\Index.cshtml"
Write(Model.IntProperty);
#line default
#line hidden
EndContext("~/Views/Test/Index.cshtml", 105, 17, false);
BeginContext("~/Views/Test/Index.cshtml", 122, 4, true);
WriteLiteral("\r\n\r\n");
EndContext("~/Views/Test/Index.cshtml", 122, 4, true);
BeginContext("~/Views/Test/Index.cshtml", 127, 31, false);
#line 10 "XXX\Views\Test\Index.cshtml"
Write(Html.Raw(#Model.StringProperty));
#line default
#line hidden
EndContext("~/Views/Test/Index.cshtml", 127, 31, false);
}
}
}
As you can see, your page code is just written to output section by section, checking Write method leads to these implementation details:
public override void Write(object value)
{
WriteTo(Output, value);
}
public static void WriteTo(TextWriter writer, object content)
{
writer.Write(HttpUtility.HtmlEncode(content)); //writer - instance of TextWriter
}
So anything that you put into your string field in the viewmodel is simply encoded with HtmlEncode method and put to the page, and it cannot be compiled in the run time with default usage of mvc features.
I am really sure, that you can do it with Mvc and Razor digging deep into the sources, but that will require a lot more time and probably a lot of good old hacks. For a fast and simple solution you can use https://github.com/Antaris/RazorEngine package. You can also check its source code for how they did this.
Here's controller code, that will rended a template using RazorEngine package:
public ActionResult Test()
{
var stringTemplate = #"
#model StackOverflow.Models.TestViewModel
<br/>
>>COMPILED
<br/>
#DateTime.UtcNow.ToString()
<br/>
Compiled model int property value:
<br/>
#Model.IntProperty
";
var viewModel = new TestViewModel
{
IntProperty = 5,
StringProperty = null
};
viewModel.StringProperty = Engine.Razor.RunCompile(stringTemplate, viewModel.GetType().ToString(), null, viewModel);
return View("Index", viewModel);
}
Basic idea here is pretty simple - pass the rendering to the component, do it in controller and pass the string you got to the ViewModel, then use #HtmlHelper.Raw to render HTML that you got from the engine.
This might work for a lot scenarios, but I would strongly recommend you not to do it unless you really need it and there are no viable alternatives. Dynamic razor templates are very hard to maintain.

JSF Navigation outcome constants

I wonder if i can replace "success" with a Constants value from a JSF library.
Backing Bean Method:
#Override
public String save() {
LOG.info("Called");
return "success";
}
For your issue, you'll find Omnifaces' <o:importConstants /> so useful (that's what I use in my own projects). That way you can import your constants file in your JSF page (I use my master page template for that).
<o:importConstants
type="com.mycompany.NavigationResults" />
This way you can access your NavigationResults values both from Java code and JSF tags (EL scope).
public abstract class NavigationResults {
public static final String SUCCESS = "success";
public static final String HOME = "home";
}
Use it in your managed beans:
public String save() {
LOG.info("Called");
return NavigationResults.SUCCESS;
}
In your buttons or links:
<h:button value="Go home" outcome="#{NavigationResults.HOME}" />

ASP MVC3 - How to load a custom user defined layout for the page from a database?

I have online form builder appplication in ASP.NET MVC3 with Razor views.
It is similar to this - https://examples.wufoo.com/forms/workshop-registration/
I need users to be able to customize the page design.
Not only to upload a custom css, but also to customize the HTML page template.
Let's say users should have complete control on Layout's HTML for their custom webform page. User should be able to edit any HTML on the page, beside the form that is included into the layout.
I'm not sure how to do that with Razor and ASP.NET MVC 3.
Is it possible to:
load layout somewhere from database as string or whatever
replace some custom tags like "FORM1_INCLUDE" to
#Html.Partial("some_non_customizable_layout_for_form1")
use the result as a valid Layout file for the user's form page
Maybe 1-3 is not the best way to do what I need.
What can you suggest for such user defined page layout approach in ASP.NET MVC 3 with Razor views?
UPDATE 1
Using VirtualPathProvider I was able to load View from database, but it just returns text like:
#inherits System.Web.Mvc.WebViewPage
<body>
#Html.EditorFor(z => z.Customer)
</body>
and doesn't process any Razor syntax at all.
What could be the problem with it?
SOLVED:
Needed to place this line as the first one in Application_Start() method:
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
UPDATE 2
Custom View Provider is registered in Global.asax.cs as:
protected void Application_Start()
{
HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
MyVirtualPathProvider code is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return true;
//return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
return new MyVirtualFile(virtualPath, "");
//return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
And now for the Razor view code defined as a string above I get this exception:
"Compiler Error Message: CS1061: 'System.Web.Mvc.AjaxHelper' does not contain a definition for 'BeginForm' and no extension method 'BeginForm' accepting a first argument of type 'System.Web.Mvc.AjaxHelper' could be found (are you missing a using directive or an assembly reference?)"
And when I changed Razor View code to:
string _body = /*body +*/ #"
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<dynamic>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""ppg_op2_personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
I got a different error:
Type 'ASP._Page__appstart_cshtml' does not inherit from 'System.Web.WebPages.ApplicationStartPage'
When I change
#inherits System.Web.Mvc.WebViewPage
to
#inherits System.Web.WebPages.ApplicationStartPage
to fix the error above, I get a new one:
"Compiler Error Message: CS0103: The name 'Ajax' does not exist in the current context"
UPDATE3:
I tried to use base.XXX:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Hosting;
using System.IO;
using System.Text;
namespace new_frontend
{
public class MyVirtualPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return true;
return base.FileExists(virtualPath);
}
else
{
return true;
}
}
public override VirtualFile GetFile(string virtualPath)
{
var td = FindTemplate(virtualPath);
if (td == null)
{
//return new MyVirtualFile(virtualPath, "");
return base.GetFile(virtualPath);
}
else
{
return new MyVirtualFile(virtualPath, td.ContentStep1);
}
}
private Facade.Dto.TemplateData FindTemplate(string virtualPath)
{
string prefix = "Template#";
int id = 0;
Facade.Dto.TemplateData td = null;
string fileName = System.IO.Path.GetFileNameWithoutExtension(virtualPath);
if (fileName.StartsWith(prefix))
Int32.TryParse(fileName.Substring(prefix.Length), out id);
if (id > 0)
td = Facade.FrontEndServices.GetTemplate(id);
return td;
}
}
public class MyVirtualFile : VirtualFile
{
private byte[] data;
public MyVirtualFile(string virtualPath, string body)
: base(virtualPath)
{ // 'System.Web.WebPages.ApplicationStartPage
string _body = /*body +*/ #"
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm(""Submit"", new AjaxOptions { UpdateTargetId = ""main"" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id=""personal_info"" class=""op2-block"">
</div>
<!-- <PERSONAL_INFO> -->";
this.data = Encoding.UTF8.GetBytes(_body);
}
public override System.IO.Stream Open()
{
return new MemoryStream(data);
}
}
}
In this case I get a view that is not parsed at all, this is what I get in the web browser:
#using System.Web.WebPages;
#using System.Web.Mvc;
#using System.Web.Mvc.Ajax;
#using System.Web.Mvc.Html;
#using System.Web.Routing;
#inherits System.Web.Mvc.WebViewPage<PPG.Facade.Dto.NewOrderPageData>
#using (Ajax.BeginForm("Submit", new AjaxOptions { UpdateTargetId = "main" }))
{
}
<!-- <PERSONAL_INFO> -->
<div id="ppg_op2_personal_info" class="op2-block">
</div>
<!-- <PERSONAL_INFO> -->
You should create a virtual path provider which fetches your custom views from a database.
There are several questions here about them. Just search for VirtualPathProvider
Updates (from my comments to the question discussion)
The VirtualPathProvider must be registered in Application_Start using HostingEnvironment.RegisterVirtualPathProvider(new MyVirtualPathProvider());
The base class MUST be called for all files that you can't currently serve. This is required since there can only be one VirtualPathProvider. (You'll see lots of strange errors otherwise)
#model directive doesn't work in files that you serve. You must use #inherits System.Web.Mvc.WebViewPage<YourNameSpace.YourModelName> instead.
IIRC you also need to override GetCacheDependency and return null for your own resources.
depending on the degree of flexibility and customization you wish to give this can be a very simple to a very time consuming task.
If you want users to simply customize the HTML on the page then you can use a WYSIWYG editor and store the raw html in the database.
In the view, use
#Html.Raw(Model.body) // Where body is the field containing the wysiwyg content
This will render the markup as-is.
To include custom tag / replacement you will have to define a list of string constants that can be inserted while using the WYSIWYG editor. These can then be search - replaced when displaying.
For example in your controller or model:
model.body.replace("$[form1]", "<form action='something' method='post' name='form1'></form>");
Of course depending on the nature of your application you might want to re-factor this into some sort of tag => markup conversion which will allow you to add more custom tags and their respective real HTML markups.
Hope this helps, Cheers!
I think you have to use something like this:
http://vibrantcode.com/blog/2010/11/16/hosting-razor-outside-of-aspnet-revised-for-mvc3-rc.html
I've done something similar & you may fine that XML is very useful. You save the layout definition as XML to the DB. That means it's easy to manipulate, either directly or by serialise/deserialise into an object model.
When you want to display the page, use XSLT to transform your XML into HTML, applying styles etc to the output.

Return to calling page from a shared page

I have a shared page in my ASP.NET MVC app that can be accessed from several different pages in my app. If I want the user to return to their original page after they have done their business on the shared page, what's the best way to figure out where the user was, and tell the app to go there?
Note that I'm currently doing this for the Cancel button on the shared page by telling onclick to use the page history:
<input type="button" value="Cancel" onclick="if (history.length == 0) { window.location='<%=Url.Action("Index", "Home") %>' } else { history.back() }" />
but this won't work for Save if the shared page updates data displayed on the original page.
try using:
System.Web.HttpContext.Current.Request.UrlReferrer
http://msdn.microsoft.com/en-us/library/system.web.httprequest.urlreferrer.aspx
Two ways I can think of are to store the url of the calling page in a hidden input element or as a argument in the current page's address. Both of these will survive refreshes or several steps of navigation, you just have to make sure that the variables are passed on to the next page.
Issues like this are why I'm more and more considering using a BaseViewModel class to inherit all of my view models from to store information that may be useful to have on any given page. I've been trying to come up with a complete class definition lately, but for these purposes it would probably be nice to have the follwing properties (posted part of this in an answer to a different question, I'll find a link in a bit):
public class BaseModel
{
public string PageTitle { get; set; }
public string PageDescription { get; set; }
public string PageKeywords { get; set; } //maybe use a List<string> or string[] here
public string ReturnPage { get; set; }
//TBD: any other useful HTML page elements
}
Then I could create a view model that inherits from this:
public class RandomViewModel : BaseViewModel
{
RandomViewModel()
{
//set page properties
}
}
Then in a service layer (preferably) or in a controller (may have to do it there, haven't tried this so can't be sure) you would have access to the ReturnPage property when constructing your model (in this example I'll assume you're doing this in the action):
public ActionResult RandomAction()
{
RandomViewModel model = _serviceLayer.GetRandomViewModel();
model.ReturnPage = this.HttpContext.Request.UrlReferrer;
return View(model);
}
Then in the view you would be able to do this:
<input type="button" value="Cancel" onclick="if (history.length == 0) { window.location='<%= Model.ReturnPage %>' } else { history.back() }" />
This is all just kind of brainstorming, but I think it would work. The only big issue I'm not sure of is where you would want to set the referrer value. I would also try and get that onclick event out of the element and set it in the header if you can.
Hope this helps.

Resources