Blazor - contextual buttons in parent component changed by child component - data-binding

I'm hoping someone can help as I'm new to Blazor.
I've started a new Blazor project and am trying to add contextual buttons in the top bar that can be changed by / based on the child component (when you navigate to page 1 it can show different things to when you navigate to page 2)
This is my approach so far:
TopBar.razor - A template for the top bar with render fragment so I can render different stuff
#inject Learning.Data.TopBarContext TopBarContext
<div class="col-sm-6">#Context</div>
<span class="col-sm-2">#Text1</span>
<span class="col-sm-2">#Text2</span>
#code {
[Parameter]
private RenderFragment Context { get; set; }
[Parameter]
public string Text1 { get; set; } = "Not Set";
[Parameter]
public string Text2 { get; set; } = "Not Set";
}
MainLayout.razor - I use TopBar and cascade it down to all the pages
#inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4 auth">
<TopBar />
</div>
<div class="content px-4">
<CascadingValue Value="TopBar" Name="TopBar">
#Body
</CascadingValue>
</div>
</div>
#code {
private TopBar TopBar;
}
Now I have to two page components:
Page1.razor - When navigated to will render 3 buttons in top bar
#page "/page1"
<button>One</button>
<button>Two</button>
<button>Three</button>
#code {
}
Page2.razor - When navigated to will render 2 buttons in top bar
#page "/page2"
<button>One</button>
<button>Two</button>
#code {
}
I know I can use the code below to pick up the cascaded value:
[CascadingParameter(Name = "TopBar")]
protected string TopBar { get; set; }
But not sure how to proceed, I was hoping for something like cascading the whole component down so that in page1 I could do something flexible like:
<TopBar>
<button>One</button>
<button>Two</button>
<button>Three</button>
</TopBar>
I may be going about this completely the wrong way (I did think creating a registered singleton might be a way to do it) but any clarification you could offer on how to accomplish this would be greatly appreciated!
Many Thanks
Nick

Looking at what you have above, you are almost there. You can cascade the TopBar like you have it in your MainLayout, but the code section where you are picking it up is what is throwing you off. You have the cascaded value shown as a string property, but remember that Blazor components are classes that are compiled, so you need to list it as a class instead:
[CascadingParameter]
public TopBar Bar { get; set; }
That is how you will capture it in your components, and then you can access the public properties and methods to update them, using code like Bar.Context = // render fragment of your choice
This might get a bit tricky though as what you feed to the Context property could prove to be painful to build up, so it might be better to have your TopBar component render something itself based on a data structure that it holds instead. I've done something with a Dictionary before for links that worked well, so in your TopBar you would have a private property:
private Dictionary<string, string> LinkBodiesAndRefs { get; set; } = new Dictionary<string, string>();
This stored link text as the key and the target URL as the value for each record,
and then to render them, something like the following:
TopBar.razor
#foreach (KeyValuePair<string, string> entry in LinkBodiesAndRefs)
{
#entry.Key
}
Then in the #code block of the TopBar, I added a few methods to interact with the dictionary:
TopBar.razor (continued)
#code {
private Dictionary<string, string> LinkBodiesAndRefs { get; set; } = new Dictionary<string, string>();
public void AddLink(string linkBody, string linkDestination)
{
LinkBodiesAndRefs.Add(linkBody, linkDestination);
StateHasChanged();
}
public void RemoveLink(string linkBody)
{
LinkBodiesAndRefs.Remove(linkBody);
StateHasChanged();
}
public void Clear()
{
LinkBodiesAndRefs = new Dictionary<string, string>();
StateHasChanged();
}
}
Then in your component that captures the cascaded TopBar, just use the methods to interact with the data structure as you would expect, whether it be a list or dictionary or whatever:
Bar.AddLink("Products", "/products");
Bar.RemoveLink("Products");
The same approach could be used with lists of objects if they need further interaction, and those could even be Razor Components and you could have a list of RenderFragment in your TopBar that gets rendered out if you wanted. The point is that the CascadingValue can provide a class that you can then define the interactivity with, and you can get up to all sorts of shenanigans to make cool things happen.
Hope this helps, and I don't have my IDE handy at the moment so there may be some syntax to work out that I missed in the above examples. Sorry in advance if that's the case.

Related

How to check markup after I get page data in OnInitializedAsync method?

I'm new to Blazor and bUnit. I have component that renders an edit form and I get the values for the form in my OnInitializedAsync event.
I'm having trouble working out how to use cut.WaitForState() or cut.WaitForAssertion().
Here's my razor code:
#page "/{AppId:guid}/app-settings-edit"
<section class="app-settings-edit">
<h1 class="page-title">Application Settings</h1>
#if (InitializedComplete)
{
<p>Hello World</p>
...
And my code behind:
public partial class AppSettingsEdit
{
protected bool InitializedComplete;
[Parameter]
public Guid AppId { get; set; }
[ValidateComplexType]
public AppSettings AppSettings { get; set; } = new AppSettings();
[Inject]
public IAppSettingsDataService AppSettingsDataService { get; set; }
protected override async Task OnInitializedAsync()
{
AppSettings = await AppSettingsDataService.Get(AppId);
InitializedComplete = true;
}
...
And here's my Test:
[Fact]
public void MyFact()
{
Services.AddSingleton<IAppSettingsDataService, MockAppSettingsDataService>(x => new MockAppSettingsDataService(x.GetRequiredService<HttpClient>()));
var cut = RenderComponent<AppSettingsEdit>(parameters => parameters
.Add(p => p.AppId, Guid.Parse("55E5097B-B56A-40D7-8A02-A5B94AAAD6E1"))
);
Assert.NotNull(cut.Instance.AppSettingsDataService);
cut.WaitForState(() => cut.Find("p").TextContent == "Hello World", new TimeSpan(0, 0, 5));
cut.MarkupMatches("<p>Hello World</p>");
}
When I debug the test, I can see the OnInitializedAsync firing, however my markup never changes to include 'Hello World' and the WaitForState() command fails.
Are you certain that the task returned from your AppSettingsDataService.Get() method ever completes?
I would make sure that the task returned from AppSettingsDataService.Get() is already completed, otherwise you need to a way to complete the task after the component is rendered. There are many ways to do this, it all depends on how your mock is implemented.
As for your WaitFor, you can just use the WaitForAssertion method in this case, i.e.: cut.WaitForAssertion(() => cut.MarkupMatches("<p>Hello World</p>"));
A little background:
The WaitFor* methods are used when the component under test is being rendered asynchronously, since the test, running in a different thread, doesn't know when that will happen.
In general, you should never need to set a custom timeout, the default is 1 second, but the WaitFor* methods will retry the assertion/predicate every time a renderer happens. Its only when the thing that triggers the rendering will take more than one second, e.g. if you are using bUnit to perform end-2-end testing and e.g. pulling data from a real web service.

How to set properties of custom controls xamarin forms in xaml

My goal is to set the properties of custom controls from a Page using xaml. I'm attempting to create a custom set of menu items, that all look the same but each one contains a different title. How could I set the title of each menu item from Xaml?
For Example sake, Ive simplified my actual code from the project to something simple.
What I want to do is something like this:
<local:CustomControl someCustomAccessor="some text">
While setting the property for the custom control like this:
class CustomControl: ContentView{
private string someText { get; set; }
public static BindableProperty TitleProperty = BindableProperty.CreateAttached("someCustomAccessor", typeof(string), typeof(CustomControl), null, BindingMode.TwoWay);
public CustomControl()
{
Content = new Label
{
Text = someText
};
}
private string someCustomAccessor
{
get { return someText; }
set { someText = value; }
}
}
I've looked everywhere for something like this and have come up short. Please help.
It's been a while since I've done something similar to this, but drawing on my Silverlight experience...
The .CreateAttached("someCustomAccessor" ....
Tells the binding engine to look for a Public property called someCustomAccessor, yours is currently private and will need to be Public. Also change the casing to be SomeCustomAccessor.
Also, the Binder doesn't know that it's a string, so it will need to be cast and set appropriately in the get;set;
public string SomeCustomAccessor
{
get { (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
Also the binding in your example is TwoWay, it may only need to be OneWay, but you can change that once you've got it working.

Asp.net MVC change css on publish

We have an MVC application that is deployed as 3 different versions in production (seperate business areas demand seperate DBs and UIs). The only problem is that a few users use more than 1 of these applications and because they look vsually similar people get confused as to which one they are using.
I use web.config transforms to change the app title but what would like to do is deliver each one with a different css file. Is there a way to:
Use transforms on publish to edit an existing css file?
or
Swap the css file for another on publish?
Any help pointing me in the right direction would be great.
Thanks
What I did in a similar case : I dynamicly put a class on the body <body class="theme-site#AppSettingParam"> and i have only one CSS with override like :
.theme-site1 { background-color:blue; }
.theme-site2 { background-color:red; }
if a user uses multiple sites, it does not re-download the css and i have only one css to maintain.
This isn't the answer you're looking for right now, but if you do reconsider using the same site with different configurations, take a look at what I did recently to dynamically change CSS etc:
I decided that each 'version' of the website would use a unique reference in the query string. Based on this, I'll find the correct content, load the paths into a Model and send it to the view.
Here's the controller:
public ActionResult Index()
{
List<string> listOfAcceptableRef = new List<string>() { "uniqueCode1", "uniqueCode2" };
string uniqueRef=null;
if (Request.QueryString["ref"]!=null)
policyRef = Request.QueryString["ref"].ToLower();
if (string.IsNullOrWhiteSpace(uniqueRef) || (!string.IsNullOrWhiteSpace(uniqueRef) && !listOfAcceptableRef.Contains(uniqueRef)))
{
throw new Exception("This reference key is unknown.");
//return RedirectToAction("KeyError");
}
return View(GetPageContext(uniqueRef));
}
Grab the reference code from the query string and then generate a model containing the relevant CSS paths from a context factory.
Here's my model:
public class PageContext
{
public string Ref { get; set; }
public string TabId { get; set; }
public string TabName { get; set; }
public string SiteTitle { get; set; }
public string CssPath { get; set; }
public PageContext()
{
Products = new List<ProductInfo>();
}
}
And my context factory:
public class ContextFactory<T>
{
private ContextFactory()
{
}
static readonly Dictionary<string, Type> _dict = new Dictionary<string, Type>()
{
{ "uniqueRef1", Type.GetType("The.Full.Page.Namespace.UniqueSite1Context")},
{ "uniqueRef2", Type.GetType("The.Full.Page.Namespace.UniqueSite2Context")}
};
public static bool Create(string reference, out T context)
{
context = default(T);
Type type = null;
if (_dict.TryGetValue(reference, out type))
{
context = (T)Activator.CreateInstance(type);
return true;
}
return false;
}
}
And the actual context instance with the CSS paths etc:
public class UniqueSite1Context : PageContext
{
public UniqueSite1Context()
{
this.Ref = "uniqueSite1";
this.CssPath = "Content/UniqueSite1Context.css";
this.DisclaimerPath = "Content/UniqueSite1Context.pdf";
this.SiteTitle = "UniqueSite1";
}
}
After all that, just render the CSS using the model's paths:
#section Styles {
#{
string path = Url.Content("~") + Model.CssPath;
<link href="#path" rel="stylesheet" type="text/css" />
}
}
Architecturally speaking, you could extend this (or rather the concept of it) to use different logic and data contexts based on which 'site' the user goes to.
Have you considered transform files? They change the settings in your web.config file when you publish, based on the publish profile selected. Typically the profiles available by default is "Debug" and "Release", but you could add more like "Site1" and "Site2" with different CSS paths.
Take a look at how they work here. (I haven't used them myself so I can't help much in the way of code examples).

Passing JSON data from controller action to a razor view

I would like to know how can I pass data from controller to view in mvc3 razor.
The view is in .cshtml file.
I have a variable in the controller where I am storing the data I need to push to the listbox control in the view.
var results = data.Select(item => new { id = item, label = item.Value, value = item.Key });
Doing this:
return Json(results, JsonRequestBehavior.AllowGet);
Gives me only a popup with the data which needs to be pushed to the listbox:
In the view the listbox resides in accordion control:
<div id="accordion">
#{int i=0;}
#foreach (var item in Model.Parameters)
{
<h3>#Html.LabelFor(m => item.Name, item.Prompt)</h3>
<div>
<div class="editor-field">
<select multiple id="#("Select" +item.Name)" name="#("Select" +item.Name)"></select>
</div>
</div>
i++;
}
</div>
So, I would like to know what should I do to push the items to the listbox instead of showing a popup for the control
Beginner in MVC, Thanks for your understanding.
Thanks in advance, Laziale
EDIT: Json format output
{System.Linq.Enumerable.WhereSelectListIterator<System.Collections.Generic.KeyValuePair<string,string>,<>f__AnonymousType1<System.Collections.Generic.KeyValuePair<string,string>,string,string>>}
returning JSON to your razor view is probably not the best method. I would suggest use a viewModel which is a c# class by itself.
namespace Test
{
public class ViewModel
{
public bool GivingAPresentation { get; set; }
}
}
public class MyController: Controller
{
public virtual ActionResult Index()
{
var model=new ViewModel(){GivingAPresentation =true;}
return View(model);
}
}
Your view code:
#model Test.ViewModel <!-- Note the full namespace -->
<br>GivingAPresentation: #Model.GivingAPresentation </br>
If you are forced to work with a JSON object that is returned from your action then you need to deserialize it first and then work with that object. you can read this post http://www.drowningintechnicaldebt.com/ShawnWeisfeld/archive/2010/08/22/using-c-4.0-and-dynamic-to-parse-json.aspx on how to parse JSON to a c# dynamic object.
Let me know if that helps.

Spring 3 MVC: how to make a form with comma-separated set of strings

I am quite new in Spring 3 and I don't know the best mean to resolve my problem.
I have an entity Idea:
public class Idea {
private Set<Tag> tags;
}
which has several related tags.
I would like to create a form in which I can give a comma-separated list of tags in one input (kind of the same as in stackoverflow).
And I need kind of a parser which splits the string with those commas and adds each tag to the idea.
How do I do that properly?
My current form is this:
<form:form modelAttribute="idea">
<form:input path="tags" />
</form:form>
And the current displayed text in the input is "[]" (must be the toString method of the HashSet class).
Btw, I use Spring 3.0.5, Hibernate and JSPs.
Edit: May I create a special-dedicated class:
public class IdeaForm {
String tags;
}
And then create an utility class which maps comma-separated tags to a Set?
Thanks!
Finally used the initBinder facility in my controller:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Set.class, new CommaDelimitedStringEditor());
}
And created my custom editor
public class CommaDelimitedStringEditor extends PropertyEditorSupport {
public void setAsText(String text) {
Set<Tag> tags = new HashSet<Tag>();
String[] stringTags = text.split(", ");
for(int i =0; i < stringTags.length; i++) {
Tag tag = new Tag();
tag.setName(stringTags[i]);
tags.add(tag);
}
setValue(tags);
}
}

Resources