How can I run some Javascript functions in a Blazor app, without using the JS interop or jQuery? Just plain old Javascript functions that interact with the DOM, independently of Blazor.
I added my script right before the closing </body> tag:
<script src="app.js"></script>
And in app.js I have the following:
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
Of course the selector finds no element. I'm guessing they aren't yet present in the DOM at that point? How can I run that script without using the JS interop or jQuery?
If your okay with using jquery you can use an on handler, which will apply to dynamically added elements
$("body").on("click", ".some-element", function(){
alert("Hello");
});
You can do natively too, but seems sketchy IMHO:
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.className.toLowerCase() === 'some-element') {
alert("Hello");
}
});
Note you have weird javascripty things to worry about now like z-index if the click event actually gets up to the body, but it should work for simple stuff.
Main question is WHY would you want to do this in Blazor - whole point is you can use C# events instead you crazy person!!
Create a page like this:
<div height="#($"{Height}px")" width="#($"{Width}px")">
Test
</div>
<button #onclick="DoubleSize">Double Size</button>
#code
{
public int Width { get; set; } = 50;
public int Height { get; set; } = 50;
private void DoubleSize()
{
Width = Width * 2;
Height = Height * 2;
}
}
On render the height and width are 50px:
Then you click the Button, and they change to 100px:
Similarly with CSS if that's what you were talking about:
<div style="width: #($"{Width}px"); height: #($"{Height}px")">
Test
</div>
This is interacting with the properties of the DOM elements without using Javascript... you can do this with any property. You don't need to use Javascript - you just bind them to your properties in C#...
You shouldn't need to 'retrieve the final properties of the rendered DOM elements' as you should be controlling them from C# and not worrying about the DOM
I'm pretty new in Blazor, so sorry if it's not the correct way to do it in Balzor. But I think that you need always use JS interop to use JavaScript function. You want to execute some script, but the question is: when do you want to execute the script? I imagine you want to execute an action after navigate to a page, after make a click in a button... and all this events happens in Blazor
If you ask about relationate an blazor element with the doom you need use #ref
A little example. You create a .js like
var auxiliarJs = auxiliarJs || {};
auxiliarJs.getBoundingClientRect = function (elementRef) {
let result = elementRef? elementRef.getBoundingClientRect():
{left: 0,top: 0,right: 0,bottom: 0,x: 0,y: 0,width: 0,height: 0 };
return result;
}
auxiliarJs.executeFunction = function (elementRef, funciones) {
let res = null;
try {
if (Array.isArray(funciones)) {
functiones.forEach(funcion => {
elementRef[funcion]()
});
}
else
res = elementRef[funciones]();
}
catch (e) { }
if (res)
return res;
}
auxiliarJs.setDocumentTitle = function (title) {
document.title = title;
};
And a service.cs with his interface
public interface IDocumentService
{
Task<ClientRect> getBoundingClientRect(ElementReference id);
Task setDocumentTitle(string title);
Task<JsonElement> executeFunction(ElementReference id, string funcion);
Task executeFunction(ElementReference id, string[] funciones);
}
public class DocumentService:IDocumentService
{
private IJSRuntime jsRuntime;
public DocumentService(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
}
public Dictionary<string, object> JSonElementToDictionary(JsonElement result)
{
Dictionary<string, object> obj = new Dictionary<string, object>();
JsonProperty[] enumerador = result.EnumerateObject().GetEnumerator().ToArray();
foreach (JsonProperty prop in enumerador)
{
obj.Add(prop.Name, prop.Value);
}
return obj;
}
public async Task<ClientRect> getBoundingClientRect(ElementReference id)
{
return await jsRuntime.InvokeAsync<ClientRect>("auxiliarJs.getBoundingClientRect", id);
}
public async Task setDocumentTitle(string title)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.setDocumentTitle", title);
}
public async Task<JsonElement> executeFunction(ElementReference id,string funcion)
{
var result= await jsRuntime.InvokeAsync<JsonElement>
("auxiliarJs.executeFunction", id, funcion);
return result;
}
public async Task executeFunction(ElementReference id, string[] funciones)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.executeFunction", id, funciones);
}
}
public class ClientRect
{
public float left{ get; set; }
public float top { get; set; }
public float right { get; set; }
public float bottom { get; set; }
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
}
Well, you inject the service as usual in program.cs
public static async Task Main(string[] args){
....
builder.Services.AddSingleton<IDocumentService, DocumentService>();
}
And in your component razor
#inject IDocumentService document
<div #ref="mydiv"></div>
<input #ref="myinput">
<button #onclick="click">click</button>
#code{
private ElementReference mydiv;
private ElementReference myinput;
click(){
ClientRect rect = await document.getBoundingClientRect(mydiv);
document.setDocumentTitle("New Title");
document.executeFunction(myinput 'focus')
}
}
Blazor "overwrites" all attached JS Events during render process
window.attachHandlers = () => {
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
and in razor page
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
{
JSRuntime.InvokeVoidAsync("attachHandlers");
}
}
Related
I'm learning JSInterop with Blazor.
I would like to get the width and the height of a Blazor page in oder to size à canvas.
I'm using the default Blazor template with the menu bar on the left part of the screen.
On the html part of the Canvas Test 1 page I have:
#page "/canvas_test1"
#inject IJSRuntime JsRuntime
<canvas #ref="canvas1"></canvas>
<button #onclick="SetCanvasSize">Set canvas size</button>
In the code part I have:
ElementReference canvas1;
async Task SetCanvasSize()
{
await JsRuntime.InvokeVoidAsync("SetCanvasSize", canvas1);
}
In the Javascript file I have:
function SetCanvasSize(element) {
ctx = element.getContext('2d');
console.log(window.innerWidth);
console.log(document.documentElement.scrollWidth);
}
But both methodes
window.innerWidth and document.documentElement.scrollWidth
give the width of the entire window not only the page that contains the canvas.
How can I get the width of only the page without the side menu?
Thank you
I think you're a little confused about what a Page is in Blazor. It's basically a component that you provide a route to for navigation. If you include a layout (which is usually set by default), that is in fact part of the page. All the things on the page, like your canvas, are just html elements. It is correctly telling you the size of the window, which you can see will change if you resize your browser.
html elements have the properties "offsetWidth" and "offsetHeight." So, for your use you want to pass the element to measure, and use element.offsetWidth:
function GetCanvasSize(element) {
alert ("Element measurements: " + element.offsetWidth + ", " + element.offsetHeight);
}
You can see more of the members of html elements here, including common methods and events: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth
Note that this list is missing some important things you'd like to know, especially the various touch events which you'll need for dragging elements on mobile clients (mouse events won't work on phones).
You need to get the dimensions of the containing element. You also need to listen for resizing events.
I use a service that can take a ref for any element and listen for changes.
bCore.js
export function listenToWindowResize(dotNetHelper) {
function resizeEventHandler() {
dotNetHelper.invokeMethodAsync('WindowResizeEvent');
}
window.addEventListener("resize", resizeEventHandler);
dotNetHelper.invokeMethodAsync('WindowResizeEvent');
}
export function getBoundingRectangle(element, parm) {
return element.getBoundingClientRect();
}
export function getWindowSizeDetails(parm) {
var e = window, a = 'inner';
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
let windowSize =
{
innerWidth: e[a + 'Width'],
innerHeight: e[a + 'Height'],
screenWidth: window.screen.width,
screenHeight: window.screen.height
};
return windowSize;
}
JSInteropCoreService.cs
public class JSInteropCoreService : IJSInteropCoreService, IAsyncDisposable
{
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
private bool isResizing;
private System.Timers.Timer resizeTimer;
private DotNetObjectReference<JSInteropCoreService> jsInteropCoreServiceRef;
public JSInteropCoreService(IJSRuntime jsRuntime)
{
this.resizeTimer = new System.Timers.Timer(interval: 25);
this.isResizing = false;
this.resizeTimer.Elapsed += async (sender, elapsedEventArgs) => await DimensionsChanged(sender!, elapsedEventArgs);
this.moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
identifier: "import", args: "./_content/BJSInteroptCore/bCore.js").AsTask());
}
public event NotifyResizing OnResizing;
public event NotifyResize OnResize;
public async ValueTask InitializeAsync()
{
IJSObjectReference module = await GetModuleAsync();
this.jsInteropCoreServiceRef = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync(identifier: "listenToWindowResize", this.jsInteropCoreServiceRef);
this.BrowserSizeDetails = await module.InvokeAsync<BrowserSizeDetails>(identifier: "getWindowSizeDetails");
}
public async ValueTask<BrowserSizeDetails> GetWindowSizeAsync()
{
IJSObjectReference module = await GetModuleAsync();
return await module.InvokeAsync<BrowserSizeDetails>(identifier: "getWindowSizeDetails");
}
public async ValueTask<ElementBoundingRectangle> GetElementBoundingRectangleAsync(ElementReference elementReference)
{
IJSObjectReference module = await GetModuleAsync();
return await module.InvokeAsync<ElementBoundingRectangle>(identifier: "getBoundingRectangle", elementReference);
}
[JSInvokable]
public ValueTask WindowResizeEvent()
{
if (this.isResizing is not true)
{
this.isResizing = true;
OnResizing?.Invoke(this.isResizing);
}
DebounceResizeEvent();
return ValueTask.CompletedTask;
}
public BrowserSizeDetails BrowserSizeDetails { get; private set; } = new BrowserSizeDetails();
private void DebounceResizeEvent()
{
if (this.resizeTimer.Enabled is false)
{
Task.Run(async () =>
{
this.BrowserSizeDetails = await GetWindowSizeAsync();
isResizing = false;
OnResizing?.Invoke(this.isResizing);
OnResize?.Invoke();
});
this.resizeTimer.Restart();
}
}
private async ValueTask DimensionsChanged(object sender, System.Timers.ElapsedEventArgs e)
{
this.resizeTimer.Stop();
this.BrowserSizeDetails = await GetWindowSizeAsync();
isResizing = false;
OnResizing?.Invoke(this.isResizing);
OnResize?.Invoke();
}
public async ValueTask DisposeAsync()
{
if (moduleTask.IsValueCreated)
{
IJSObjectReference module = await GetModuleAsync();
await module.DisposeAsync();
}
}
private async Task<IJSObjectReference> GetModuleAsync()
=> await this.moduleTask.Value;
}
public class ElementBoundingRectangle
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double Top { get; set; }
public double Right { get; set; }
public double Bottom { get; set; }
public double Left { get; set; }
}
public class BrowserSizeDetails
{
public double InnerWidth { get; set; }
public double InnerHeight { get; set; }
public int ScreenWidth { get; set; }
public int ScreenHeight { get; set; }
}
I debounce the resize events using a 25ms interval to prevent lag.
I am writing a modal component for blazor but i am struggeling to find a solution which fulfills the requirement to do both at the same time:
close ONLY when clicked outside modal content
update C# object state (Visibility flag) accordingly
scenario 1
If i use the C# based approach with #onclick="WrapperClicked" i can update the Visibility state very easy, but struggle to get the DOM click event and therefore cannot distinguish between a wrapper and a wrapper content click.
The c# MouseEventArgs do not contain properties to distinguish the clicked dom element.
scenario 2
Is based around uncommenting the code for
private Dictionary<string, object> ComponentValues()
{
var values = new Dictionary<string, object>();
// if(CloseOnModalFrameClickInternal)
// values.Add("onclick", "Amusoft.Components.ModalDialog.closeEvent(this, event);");
return values;
}
With this version it is simple to get access to the dom click event - but i cannot pass a DotNetObjectReference to that handler, to be able to call back into c# and update my components state.
Question
Does anyone have ideas how to resolve this deep interop scenario?
typescript code to distinguish wrapper click from wrapper content click:
public static closeEvent(dotNetHelper: any, event: MouseEvent): void {
console.log(event);
console.log(dotNetHelper);
let target = event.target as HTMLElement;
if(target != null && target?.classList?.contains("amu-modal-wrapper")){
// element.style.setProperty("display", "none");
console.log("breakpoint landed");
// valueOfReference.invokeMethodAsync("JsSetVisibility", false);
}
}
Additional Scss code compared to default code:
.amu-modal-wrapper {
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 20px;
.amu-modal-content {
width: 100%;
height: 100%;
background-color: orange;
}
}
Modal component code:
#using Microsoft.JSInterop
#implements IDisposable
<div #ref="wrapper" class="amu-modal-wrapper" #attributes="ComponentValues()" #onclick="WrapperClicked" style="padding: #(Padding)px; display: #(VisibleInternal ? "block" : "none")">
<div class="amu-modal-content">
<h3>#Headline</h3>
#ChildContent
</div>
</div>
#code {
private ElementReference wrapper;
private DotNetObjectReference<ModalDialog> _self;
[Parameter]
public string Headline
{
get => HeadlineInternal;
set => HeadlineInternal = value;
}
[Parameter]
public EventCallback<string> HeadlineChanged { get; set; }
private string _headlineInternal;
private string HeadlineInternal
{
get { return _headlineInternal; }
set
{
if (EqualityComparer<string>.Default.Equals(_headlineInternal, value))
return;
_headlineInternal = value;
HeadlineChanged.InvokeAsync(value);
}
}
[Parameter]
public RenderFragment ChildContent { get; set; }
[Inject]
public IJSRuntime JsRuntime { get; set; }
[Parameter]
public int Padding { get; set; } = 100;
[Parameter]
public bool Visible
{
get => VisibleInternal;
set => VisibleInternal = value;
}
[Parameter]
public EventCallback<bool> VisibleChanged { get; set; }
private bool _visibleInternal;
private bool VisibleInternal
{
get { return _visibleInternal; }
set
{
if (EqualityComparer<bool>.Default.Equals(_visibleInternal, value))
return;
_visibleInternal = value;
VisibleChanged.InvokeAsync(value);
}
}
[Parameter]
public bool CloseOnModalFrameClick
{
get => CloseOnModalFrameClickInternal;
set => CloseOnModalFrameClickInternal = value;
}
private Dictionary<string, object> ComponentValues()
{
var values = new Dictionary<string, object>();
// if(CloseOnModalFrameClickInternal)
// values.Add("onclick", "Amusoft.Components.ModalDialog.closeEvent(this, event);");
return values;
}
private bool CloseOnModalFrameClickInternal { get; set; } = true;
public void Hide()
{
VisibleInternal = false;
StateHasChanged();
}
public void Show()
{
VisibleInternal = true;
StateHasChanged();
}
protected override void OnInitialized()
{
_self = DotNetObjectReference.Create(this);
base.OnInitialized();
}
protected override void OnAfterRender(bool firstRender)
{
base.OnAfterRender(firstRender);
if (firstRender)
JsRuntime.InvokeVoidAsync("Amusoft.Components.ModalDialog.initialize", wrapper);
}
public void Dispose()
{
_self?.Dispose();
}
private Task WrapperClicked(MouseEventArgs arg)
{
JsRuntime.InvokeVoidAsync("Amusoft.Components.ModalDialog.closeEvent", _self, arg);
return Task.CompletedTask;
}
}
You dont need Java.
<div class="modal-outer" #onclick="OnBackgroundClicked">
<div class="modal-inner" #onclick:stopPropagation="true">
From my repo
It is now a free nuget package as of about 24hrs ago
I am loading images from Wikipedia into a Grid view. For the most part this is working correctly. Because there could possible be up to 200 or more images being loaded I am try to run it in a new thread. I see a definite delay when scrolling from my Album tab to the Artist tab that is loading the images. I am also see some lag as images are still getting load while scrolling up and down the list. Also when I scroll back to the top of the list place holders that previously occupied by the default image because I am unable to get an image from Wikipedia are now occupied by images from another artist.
When I scroll back to the song list and then back to the artist list the view is reset but it still has a lot of delay when going into the artist tab.
This image is what the screen looks like when first entering the Artist tab.
This image is what the screen looks like after scrolling to the bottom of the list and back to the top.
As you can see the <unknow. and AJR have had their default image replaced.
Here is my code that I am calling to load the images from Wikipedia.
#Override
public void onBindViewHolder(#NonNull ARV holder, int position) {
Artist artist = artistList.get(position);
if(artist!=null) {
holder.artistName.setText(artist.artistName);
String bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
ImageLoader.getInstance().displayImage(imageUrl, holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(true).build());
} catch (IOException e) {
e.printStackTrace();
}
/*ImageLoader.getInstance().displayImage(getCoverArtPath(context,artist.id),holder.artistImage,
new DisplayImageOptions.Builder().cacheInMemory(true).showImageOnLoading(R.drawable.album)
.resetViewBeforeLoading(false).build());*/
}
}
private StringBuilder getUrlSource(String site) throws IOException {
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
URL localUrl = null;
localUrl = new URL(site);
URLConnection conn = localUrl.openConnection();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line = "";
String html;
StringBuilder ma = new StringBuilder();
while ((line = reader.readLine()) != null) {
ma.append(line);
Log.i(ContentValues.TAG, "StringBuilder " + ma);
}
Log.i(ContentValues.TAG, "Final StringBuilder " + ma);
return ma;
}
public static String cutImg(StringBuilder split){
int start=split.indexOf("\"source\":")+new String("\"source\":\"").length();
split.delete(0, start);
split.delete(split.indexOf("\""), split.length());
Log.i(ContentValues.TAG, "StringBuilder " + split);
return split.toString();
}
Here is the code that is call the Artist Fragment.
public class ArtistFragment extends Fragment {
int spanCount = 3; // 2 columns
int spacing = 20; // 20px
boolean includeEdge = true;
private RecyclerView recyclerView;
private ArtistAdapter adapter;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_artist, container, false);
recyclerView = view.findViewById(R.id.artistFragment);
recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
Thread t = new Thread()
{
public void run()
{
// put whatever code you want to run inside the thread here.
new LoadData().execute("");
}
};
t.start();
return view;
}
public class LoadData extends AsyncTask<String, Void, String> {
#Override
protected String doInBackground(String... strings) {
if(getActivity()!=null) {
adapter=new ArtistAdapter(getActivity(),new ArtistLoader().artistList(getActivity()));
}
return "Executed";
}
#Override
protected void onPostExecute(String s) {
recyclerView.setAdapter(adapter);
if(getActivity()!=null) {
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
}
}
#Override
protected void onPreExecute() {
super.onPreExecute();
}
}
}
I have also tried this using Picasso using the following code:
bandName = artist.artistName;
bandName = bandName.replace(' ','_');
try {
String imageUrl = cutImg(getUrlSource("https://en.wikipedia.org/w/api.php?action=query&titles="+bandName+"&prop=pageimages&format=json&pithumbsize=250"));
URL url = new URL(imageUrl);
Picasso.get().load(imageUrl).placeholder(R.drawable.album)
.error(R.drawable.artistdefault).into(holder.artistImage);
} catch (IOException e) {
e.printStackTrace();
}
The results are pretty much the same as when I used Android-Universal-Image-Loader. I have been try for several days to fix this, I have tried several different examples that I found on Stack overflow but none of them seem to resolve the issues I am seeing. I am hoping that someone will be able to identify what I am doing incorrectly.
Thanks in advance.
ArtistFragmentconverted to Kotlin
class ArtistFragment : Fragment() {
var spanCount = 3 // 2 columns
var spacing = 20 // 20px
var includeEdge = true
var retrofit: Retrofit? = null
var wikiService: WikiService? = null
var adapter: ArtistAdapter? = null
private var recyclerView: RecyclerView? = null
private var viewModelJob = Job()
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private var progress_view: ProgressBar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_artist, container, false)
recyclerView = view.findViewById(R.id.artistFragment)
recyclerView?.setLayoutManager(GridLayoutManager(activity, 3))
progress_view = view.findViewById(R.id.progress_view)
initWikiService()
initList()
//LoadData().execute("")
return view
}
override fun onDestroy() {
super.onDestroy()
viewModelJob.cancel()
}
private fun initList() {
recyclerView!!.addItemDecoration(GridSpacingItemDecoration(spanCount, spacing, includeEdge))
adapter = ArtistAdapter(this)
adapter?.items = ArrayList()
adapter?.listener = this
recyclerView?.adapter = adapter
viewModelScope.launch {
progress_view.visibility = View.VISIBLE
val wikiPages = getWikiPages()
adapter?.items = wikiPages
progress_view?.visibility = View.GONE
}
}
private suspend fun getWikiPages(): ArrayList<Artist> {
val newItems = ArrayList<Artist>()
withContext(Dispatchers.IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Artist(id, title, albumCount = 0, songCount = 0, artistUrl = url!!))
}
}
}
}
return newItems
}
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
I believe I have resolved most of the issues I was previously seeing I am now down to the following problems:
Artist.item.map { artist -> - Not sure how this should be called, Unresolved reference: item
}.awaitAll().forEach { response -> = forEach is telling me Overload resolution ambiguity. All these functions match.
public inline fun Iterable<TypeVariable(T)>.forEach(action: (TypeVariable(T)) → Unit): Unit defined in kotlin.collections
public inline fun <K, V> Map<out TypeVariable(K), TypeVariable(V)>.forEach(action: (Map.Entry<TypeVariable(K), TypeVariable(V)>) → Unit): Unit defined in kotlin.collections
newItems.add(Artist(id, title, url)) - I know that the variables for the Artist Model need to go here, but when I put them there they are unresolved.
I have reworked the ArtistAdapter not sure if it is correct though.
class ArtistAdapter(private val context: ArtistFragment, private val artistList: List<Artist>?) : RecyclerView.Adapter<ArtistAdapter.ARV>() {
private var dimension: Int = 64
init {
val density = context.resources.displayMetrics.density
dimension = (density * 64).toInt()
hasStableIds()
}
var items: MutableList<Artist> = ArrayList()
set(value) {
field = value
notifyDataSetChanged()
}
var listener: Listener? = null
interface Listener {
fun onItemClicked(item: Artist)
abstract fun ArtistAdapter(context: ArtistFragment): ArtistAdapter
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ARV {
return ARV(LayoutInflater.from(parent.context).inflate(R.layout.artist_gride_item, parent,
false))
}
override fun onBindViewHolder(holder: ARV, position: Int) {
holder.onBind(getItem(position))
}
private fun getItem(position: Int): Artist = items[position]
override fun getItemId(position: Int): Long = items[position].id
override fun getItemCount(): Int {
return artistList?.size ?: 0
}
inner class ARV(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val artistNameView: TextView = itemView.findViewById(R.id.artistName)
private val artistAlbumArtView: SquareCellView = itemView.findViewById(R.id.artistAlbumArt)
fun onBind(item: Artist) {
artistNameView.text=item.artistName
if(item.artistURL!=null) {
Picasso.get()
.load(item.artistURL)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.artistdefualt)
.into(artistAlbumArtView)
} else {
artistAlbumArtView.setImageResource(R.drawable.artistdefualt)
}
itemView.setOnClickListener(this)
}
override fun onClick(view: View) {
val artistId = artistList!![bindingAdapterPosition].id
val fragmentManager = (context as AppCompatActivity).supportFragmentManager
val transaction = fragmentManager.beginTransaction()
val fragment: Fragment
transaction.setCustomAnimations(R.anim.layout_fad_in, R.anim.layout_fad_out,
R.anim.layout_fad_in, R.anim.layout_fad_out)
fragment = ArtistDetailsFragment.newInstance(artistId)
transaction.hide(context.supportFragmentManager
.findFragmentById(R.id.main_container)!!)
transaction.add(R.id.main_container, fragment)
transaction.addToBackStack(null).commit()
}
}
}
Logcat Snippet
java.lang.NoSuchMethodError: No static method metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; in class Ljava/lang/invoke/LambdaMetafactory; or it
s super classes (declaration of 'java.lang.invoke.LambdaMetafactory' appears in /apex/com.android.art/javalib/core-oj.jar)
at okhttp3.internal.Util.<clinit>(Util.java:87)
at okhttp3.internal.Util.skipLeadingAsciiWhitespace(Util.java:321)
at okhttp3.HttpUrl$Builder.parse(HttpUrl.java:1313)
at okhttp3.HttpUrl.get(HttpUrl.java:917)
at retrofit2.Retrofit$Builder.baseUrl(Retrofit.java:506)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.initWikiService(ArtistFragment.kt:103)
at com.rvogl.androidaudioplayer.fragments.ArtistFragment.onCreateView(ArtistFragment.kt:43)
It looks to me as a known bug with Picasso.
Try to load default image manually so it won't be replaced with cached one.
Update 14.10.20:
I think the main problem is that you load network content in adapter in rather ineffective way. I suggest to form a list of all urls at first, leaving only image load in adapter.
Also reccomend you to use rerofit2 for network calls and something for async work instead of AsyncTask: rxJava, courutines, flow etc.
I created a sample project to load data async using retrofit2+coroutines.
In activity:
private val viewModelScope = CoroutineScope(Dispatchers.Main)
private fun initWikiService() {
retrofit = Retrofit.Builder()
.baseUrl("https://en.wikipedia.org/")
.addConverterFactory(GsonConverterFactory.create())
.build()
wikiService = retrofit?.create(WikiService::class.java)
}
private fun initList() {
viewModelScope.launch {
val wikiPages = getWikiPages()
adapter?.items = wikiPages
}
}
private val viewModelScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private suspend fun getWikiPages(): ArrayList<Item> {
val newItems = ArrayList<Item>()
withContext(IO) {
ArtistData.artists.map { artist ->
async { wikiService?.getWikiData(artist) }
}.awaitAll().forEach { response ->
val pages = response?.body()?.query?.pages
pages?.let {
for (page in pages) {
val value = page.value
val id = value.pageid?.toLong() ?: value.title.hashCode().toLong()
val title = value.title ?: "Unknown"
val url = value.thumbnail?.source
newItems.add(Item(id, title, url))
}
}
}
}
return newItems
}
In viewHolder:
fun onBind(item: Item) {
if (item.url != null) {
Picasso.get()
.load(item.url)
.resize(dimension, dimension)
.centerCrop()
.error(R.drawable.ic_baseline_broken_image_24)
.into(pictureView)
} else {
pictureView.setImageResource(R.drawable.ic_baseline_image_24)
}
}
In adapter: add hasStableIds() to constructor and override getItemId method:
init {
hasStableIds()
}
override fun getItemId(position: Int): Long = items[position].id
Retrofit Service:
interface WikiService {
#GET("/w/api.php?action=query&prop=pageimages&format=json&pithumbsize=250")
suspend fun getWikiData(#Query("titles") band: String): Response<WikipediaResponse?>
}
I have a simple SignalR proxy with a single client method on it. The javascript looks like the following:
var proxy = $.connection.orderStatusUpdateEmitter;
proxy.client.onOrderUpdated = function(order){
try {
//This is only hit sometimes
getCustomerOrders(userId)
} catch (e) {
}
}
proxy.connection.start().done(function(c){
$log.info('Connected to order status update emitter');
});
proxy.connection.disconnected = function(data){
$log.info('disconnected');
setTimeout(function() {
proxy.connection.start();
},20000);
}
It seems like there is some type of race condition or I am doing this incorrectly because the handler for onOrderUpdated is not consistently hit. Anything glaringly wrong with the implementation above?
There is nothing special happening in my hub on the server, it looks like the following:
[AuthorizeClaims]
public class OrderStatusUpdateEmitter : Hub
{
private static string _groupIdentifier = "OrderStatusUpdate";
public override Task OnConnected()
{
var identity = Context.Request.Environment["user.identity"] as AuthenticatedUserIdentity;
Groups.Add(Context.ConnectionId, string.Format("{0}-{1}", _groupIdentifier, identity.UserId));
return base.OnConnected();
}
public override Task OnReconnected()
{
var identity = Context.Request.Environment["user.identity"] as AuthenticatedUserIdentity;
Groups.Add(Context.ConnectionId, string.Format("{0}-{1}", _groupIdentifier, identity.UserId));
return base.OnReconnected();
}
public static string GetGroupIdentifier()
{
return _groupIdentifier;
}
public Order OnOrderUpdate(Order order)
{
Clients.Group(String.Format("{0}-{1}",GetGroupIdentifier(),
order.CustomerId)).onOrderUpdated(obj);
}
}
Try the following instead of Group
list is consisting of users with ',' separator.
string[] group = list.Split(',');
for(int i=0; i < group.length; i++)
Clients.User(group[i]).onOrderUpdated(obj);
My forms have inputs with default helper text that guides the user on what to enter (rather than using labels). This makes validation tricky because the input value is never null.
How can I extend unobtrusive validation to handle this? The form shouldn't be valid if the Name input is equal to "Please enter your name..."
I started reading Brad Wilson's blog post on validation adapters, but I'm not sure if this is the right way to go? I need to be able to validate against different default values depending on the field.
Thanks
Here's a sample illustrating how you could proceed to implement a custom validation attribute:
public class NotEqualAttribute : ValidationAttribute, IClientValidatable
{
public string OtherProperty { get; private set; }
public NotEqualAttribute(string otherProperty)
{
OtherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(OtherProperty);
if (property == null)
{
return new ValidationResult(
string.Format(
CultureInfo.CurrentCulture,
"{0} is unknown property",
OtherProperty
)
);
}
var otherValue = property.GetValue(validationContext.ObjectInstance, null);
if (object.Equals(value, otherValue))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "notequalto",
};
rule.ValidationParameters["other"] = OtherProperty;
yield return rule;
}
}
and then on the model:
public class MyViewModel
{
public string Prop1 { get; set; }
[NotEqual("Prop1", ErrorMessage = "should be different than Prop1")]
public string Prop2 { get; set; }
}
controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
Prop1 = "foo",
Prop2 = "foo"
});
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
and view:
#model MyViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
jQuery.validator.unobtrusive.adapters.add(
'notequalto', ['other'], function (options) {
options.rules['notEqualTo'] = '#' + options.params.other;
if (options.message) {
options.messages['notEqualTo'] = options.message;
}
});
jQuery.validator.addMethod('notEqualTo', function(value, element, param) {
return this.optional(element) || value != $(param).val();
}, '');
</script>
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Prop1)
#Html.EditorFor(x => x.Prop1)
#Html.ValidationMessageFor(x => x.Prop1)
</div>
<div>
#Html.LabelFor(x => x.Prop2)
#Html.EditorFor(x => x.Prop2)
#Html.ValidationMessageFor(x => x.Prop2)
</div>
<input type="submit" value="OK" />
}
Yes thats the right way to go. You should implement your own atribute and implement IClientValidatable.
You could also have a required boolean value set initially to false as a hidden form field. When the user changes the textbox, set it to true.
You could make your ViewModel implement IValidatableObject and when implementing the Validate method (from IValidatableObject) add some logic to check the values of the properties e.g.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
var results = new List<ValidationResult>();
if (Name == "Please enter your name...")
results.Add(new ValidationResult("You must enter a name");
...
Enter other validation here
...
return results;
}
Now, when Model.IsValid is called in your controller, this bit of logic will be ran and will return validation errors as normal.
It took a little while since your question was asked, but if you still like data annotations, this problem can be easily solved using this library:
[Required]
[AssertThat("FieldA != 'some text'")]
public string FieldA { get; set; }
Above, the field value is compared with some pre-defined text. Alternatively, you can compare fields values with each other:
[AssertThat("FieldA != FieldB")]
...and when the case of the strings being compared does not matter:
[AssertThat("CompareOrdinalIgnoreCase(FieldA, FieldB) != 0")]
To improve a little bit of #Darin Dimitrov answer, if you want to add messages from the resources using ErrorMessageResourceName and ErrorMessageResourceType, just add this to the to the Error message ErrorMessage = ErrorMessage ?? ErrorMessageString
The ErrorMessageString will look for the localized version of error message that you set in the model using those parameters (ErrorMessageResourceName and ErrorMessageResourceType)
The ideal solutions is a custom Attribute where you specify minimum and maximum lengths as well as MustNotContain="Please enter your name...".