We are using PageTypeBuilder to define our PageTypes, on one page we have a property which represents a Link item collection as below:
[PageTypeProperty(Type = typeof(PropertyLinkCollection), HelpText = "Test links.", EditCaption = "Test links", SortOrder = 11)]
public virtual LinkItemCollection PageLinks { get; set; }
We can populate this in CMS editor mode with links, save and publish without any errors. We then have a user control that inherits from EPiServer.UserControlBase and grabs the LinkItemCollection property using the below code and binds it to a repeater:
var links = currentPage.Property["PageLinks"].Value as LinkItemCollection;
if (links != null)
{
linkRepeater.DataSource = links;
linkRepeater.DataBind();
}
If I view the page when logged in as a CMS editor this page works fine and the links parameter is populated correctly, however if I view the page as a normal user and not logged in the links variable is always null (although when I'm debugging I can see the currentPage.Property["PageLinks"] is present, and the type is LinkCollection, its just that the Value is null
Is there something I need to configure here, permissions on a specific page type?
The problem is most likely one of the pages in the LinkItemCollection not being accessible by outside visitors. Try accessing the links in the collection as an outside visitor and remove any of the links that are in fact locked from outside view.
Related
In page edit mode I want to show a read-only text that is based on a page property value. The text could for example be "A content review reminder email will be sent 2015-10-10", where the date is based on the page published date + six months (a value that will be configurable and therefore can change anytime). So far I've tried to accomplish something like this by adding another property on the page.
I've added the property CurrentReviewReminderDate to an InformationPage class we use. In page edit mode the property name is shown, but it doesn't have a value. How do I do to show the value in page edit mode (preferably as a label)?
[CultureSpecific]
[Display(
Name = "Review reminder date",
Description = "On this date a reminder will be sent to the selected mail to remember to verify page content",
Order = 110)]
[Editable(false)]
public virtual string CurrentReviewReminderDate
{
get
{
var daysUntilFirstLevelReminder =
int.Parse(WebConfigurationManager.AppSettings["PageReviewReminder_DaysUntilFirstLevelReminder"]);
if (CheckPublishedStatus(PagePublishedStatus.Published))
{
return StartPublish.AddDays(daysUntilFirstLevelReminder).ToString();
}
return "";
}
set
{
this.SetPropertyValue(p => p.CurrentReviewReminderDate, value);
}
}
EPiServer internally uses the GetPropertyValue method (i.e. the opposite of SetPropertyValue) when retrieving content for the UI.
This makes sense, otherwise your "made-up" value would be stored as the real value whenever the content is saved. This would make fall-back values etc impossible to implement.
So, this is by-design (and quite wisely so) in EPiServer. :)
However, you can customize how properties work by:
Using custom editors by applying UI hints
Modifying property metadata (for example, to display a generated value as a watermark in a textbox without interfering with the actual value being saved)
I could be misunderstanding what you're trying to do, but off the top of my head it looks like a custom editor could be a viable option for your use case?
Another solution would be to hook into the LoadedPage-event and add the value from there. This might not be the best way performance-wise since you need to do a CreateWritableClone, but depending on the site it might not matter.
[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class EventInitialization : IInitializableModule
{
public void Initialize(InitializationEngine context)
{
ServiceLocator.Current.GetInstance<IContentEvents>().LoadedContent += eventRegistry_LoadedContent;
}
void eventRegistry_LoadedContent(object sender, ContentEventArgs e)
{
var p = e.Content as EventPage;
if (p != null)
{
p = p.CreateWritableClone() as EventPage;
p.EventDate = p.StartPublish.AddDays(10);
e.Content = p;
}
}
}
In MVC, there's a ViewSwitcher, and you can add _Layout, _Layout.mobile; MyView and optional MyView.mobile
What's the best way to accomplish this in ServiceStack razor view? Thanks
ServiceStack doesn't implicitly switch layouts at runtime, instead the preferred layout needs to be explicitly set. ServiceStack's RazorRockstars Demo website explains how to dynamically switch views, i.e:
Change Views and Layout templates at runtime
The above convention is overrideable where you can change both what View and Layout Template is used at runtime by returning your Response inside a decorated HttpResult:
return new HttpResult(dto) {
View = {viewName},
Template = {layoutName},
};
This is useful whenever you want to display the same page in specialized Mobile and Print Preview website templates. You can also let the client change what View and Template gets used by attributing your service with the ClientCanSwapTemplates Request Filter Attribute:
[ClientCanSwapTemplates]
public class RockstarsService : RestServiceBase { ... }
Which itself is a very simple implementation that also shows you can you can swap the View or Template used inside a Request Filter:
public class ClientCanSwapTemplatesAttribute : RequestFilterAttribute
{
public override void Execute(IHttpRequest req, IHttpResponse res, object requestDto)
{
req.Items["View"] = req.GetParam("View");
req.Items["Template"] = req.GetParam("Template");
}
}
This attribute allows the client to change what View gets used with the View and Template QueryString or FormData Request Params. A live example of this feature is used to change the /rockstars page:
/rockstars?View=AngularJS
/rockstars?Template=SimpleLayout
/rockstars?View=AngularJS&Template=SimpleLayout
Changing Layout used from inside a view
You can even change the layout used by setting the Layout property from inside a Razor View, e.g:
#inherits ViewPage<Response>
#{
Layout = IsMobileRequest(base.Request) ? "_LayoutMobile" : "_Layout";
}
I need to bind the LinkURL of the Blog Page with a link button on the Start Page. What I did was actually found that Page ID and get a Page Reference using it.
PageReference BlogPageReference = new PageReference(21);
PageData BlogPage = GetPage(BlogPageReference);
var url = BlogPage.LinkURL;
This is pretty straight forward, but I'm not happy that the Page ID is hard coded.
Is there a better way of doing this, like getting the Page by Page name? or any other way?
Thanks in advance :)
I would create a property on the start page of type "Page", which means the property will have the type PageReference. Then it's no longer hardcoded.
It's also common to move such "settings" properties to a separate Settings page type which is itself linked via a property from the root or startpage (which are constants).
Im writing from memory so excuse any mistakes in the code.
var startPage = DataFactory.Instance.Get<StartPage>(PageReference.StartPage);
var settingsPage = DataFactory.Instance.Get<SettingsPage>(startPage.SettingsPage);
var blogPageRef = settingsPage.BlogPage;
Where SettingsPage and BlogPage are defined
public virtual PageReference xxxPage {get; set; }
in your page type class.
I use the MVVM Light Toolkit to define the association between the view-model and the view.
The container is instructed to register a view-model as a singleton instance. Thus, the same instance will always be returned when the GagaViewModel is required:
public GagaViewModel GagaViewModel
{
get
{
var vm = ServiceLocator.Current.GetInstance<GagaViewModel>();
vm.Setup(); //Clear the ObservableCollection
return vm;
}
}
You can click on a thumbnail item on PriorGaga.xml. The self-chosen item is then selected in the GridView "MyGridView" in Gaga.xaml. Code-behind file of Gaga.xaml:
protected override async void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
var itemId = navigationParameter as String;
if (String.IsNullOrEmpty(itemId))
{
throw new ArgumentException("navigationParameter was either null or empty");
}
await ((GagaViewModel)DataContext).Init(itemId); //Busy(-Indicator) while loading data from server, filling the ObservableCollection and writing the selected item down
BringItemIntoView();
}
private void BringItemIntoView()
{
var vm = (GagaViewModel)DataContext;
Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
() => MyGridView.ScrollIntoView(vm.SelectedItem));
}
That works fine. As a sample: Item #45 appears within the viewport immediately (correct viewport position from the beginning).
But when you click the back button and return to Gaga.xaml by selecting an arbitrarily thumbnail item (let's just say #29), you will see item #1 and then the switch to #29 (the viewport is moving over the container). Do someone know what's going on under there? Are there any virtualized items in the container from the preceding Gaga.xaml visit?
My understanding is that the lifespan of the instance of your Gaga page is determined by its NavigationCacheMode property. By default, it is set to Disabled. Assuming that you haven't changed this property, you should be seeing a new instance of your Gaga page every time you navigate to it. You can verify this behavior by setting a breakpoint in its constructor. Consequently, I would think that each time you navigate to Gaga, the behavior of the UI should be identical, because everything is fresh.
(I wanted to add this as a comment, since I haven't actually answered your question, but sadly I do not have enough rep. I apologize in advance; please do not smite me down!)
I'm using PRISM 4 Navigation API with Unity in WPF. I have a tree-view that initiates a RequestNavigate passing in the selected tree node's ID (GUID).
_regionManager.RequestNavigate(RegionNames.DetailRegion,
ViewNames.SiteView + "?ID=" + site.ID);
In my module, I have registered the view/view-model like so:
_container.RegisterType<SiteDetailsViewModel>();
_container.RegisterType<object, SiteDetailsView>(ViewNames.SiteView);
When I select different nodes from the tree view, the DetailsRegion displays the SiteDetailsView as expected, but when I like to navigate back to the same node, a new view/view-model is created.
I tried to break at IsNavigationTarget(NavigationContext navigationContext) but this method appears to never be called.
Where have i gone wrong? Thanks in advance.
The problem was in such a place that I never expected... Debugging the Navigation API lead me to the RegionNavigationContentLoader
public object LoadContent(IRegion region, NavigationContext navigationContext)
When i stepped further down the code, I noticed a call to:
protected virtual IEnumerable<object> GetCandidatesFromRegion(
IRegion region,
string candidateNavigationContract)
I noticed that the naming here is key to matching the view to the view-model.
In my example, the name for each part was:
public class SiteDetailsViewModel { ... } // ViewModel
public class SiteDetailsView { ... } // View
ViewNames.SiteView = "SiteView" // ViewName constant
When I inadvertently made the following change:
ViewName.SiteView = "SiteDetailsView"
Everthing worked.
Conclusion
The name of the ViewModel must start
with the same name you used to
identify your view.
I tested this out by changing my view to:
public class MyView { ... }
and still using the same view name to register with the container and navigation:
_container.RegisterType<object, MyView>(ViewNames.SiteView);
...
_regionManager.RequestNavigate(RegionNames.DetailRegion,
ViewNames.SiteView + "?ID=" + site.ID);
This seems to work also. So it seems the name of the View-Model is intrinsically linked to the view name used to navigate to that view.
NOTE
This is only when you're using IoC and Unity with the PRISM 4 Navigation API. This doesn't seem to happen when using MEF.
Further Investigation
I am also aware that some guides have told us to use the typeof(MyView).FullName when registering the view with the Container...
_container.RegisterType<object, MyView>(typeof(MyView).FullName);
I personally think this is a mistake. By using the view's full name, you are creating a depending between the view and any one who wishes to navigate to that view...
_regionManager.RequestNavigate(RegionNames.DetailRegion,
typeof(MyView).FullName + "?ID=" + site.ID);
The registration of the View and the ViewModel is the problem. To have only one view you have to use a different lifetime manager. Without specifying a lifetime manager the TransientLifetimeManager is used which always returns a new instance on resolve. To have only one single instance you have to use the ContainerControlledLifetimeManager or the HierarchicalLifetimeManager:
_container.RegisterType<SiteDetailsViewModel>(new ContainerControlledLifetimeManager());
_container.RegisterType<object, SiteDetailsView>(ViewNames.SiteView, new ContainerControlledLifetimeManager());