I have an issue with my TableView and its items. I have created a small Dialog window to display warnings about my app, and inside the Dialog I have a TableView which displays the name of the warning and some information about it upon clicking on a button.
I have created a WarningUtil class (Singleton pattern) just to open / close the Dialog. The relevant code follows.
The constructor of the WarningUtil class (called once only) :
private WarningUtil(RootCtrl rootCtrl) {
this.rootCtrl = rootCtrl;
warnings = new HashMap<>();
setupWarningCallbacks(); // not relevant
setupTable();
setupColumns(); // not relevant
setupDialog();
}
The function managing the construction of the Dialog :
private void setupTable() {
// create the content pane
content = new AnchorPane(); // class variable - reference needed for further uses
content.setPrefSize(480, 240);
// create the root nodes of the view (table + 2 columns)
warningTable = new TableView<>(); // class variable - reference needed for further uses
warnDescriptionCol = new PTableColumn<>(); // class variable - reference needed for further uses
warnDetailsCol = new PTableColumn<>(); // class variable - reference needed for further uses
// settings anchors to keep the ration between dialog <-> table
AnchorPane.setBottomAnchor(warningTable, 15.0);
AnchorPane.setTopAnchor(warningTable, 15.0);
AnchorPane.setLeftAnchor(warningTable, 15.0);
AnchorPane.setRightAnchor(warningTable, 15.0);
// setting up the columns
warnDescriptionCol.setText(i18n("label.desc"));
warnDetailsCol.setText(i18n("label.details"));
warnDescriptionCol.setPercentageWidth(0.7);
warnDetailsCol.setPercentageWidth(0.3);
warnDescriptionCol.setResizable(false);
warnDetailsCol.setResizable(false);
// adding nodes to containers
warningTable.getColumns().addAll(warnDescriptionCol, warnDetailsCol);
content.getChildren().add(warningTable);
}
The function used to create the Dialog and set the content :
private void setupDialog() {
// creation and saving of the dialog in a variable reused later
warningDialog = DialogFactory.getInstance(rootCtrl.getPrimaryStage()).createWarningDialog();
warningDialog.getDialogPane().setContent(content);
warningDialog.getDialogPane().getScene().getWindow().sizeToScene();
}
// The DialogFactory function creating the dialog
public Dialog createWarningDialog(){
CustomDialog dialog = new CustomDialog(rootStage);
dialog.setTitle(i18n("warning.description"));
ButtonType cancelBt = new ButtonType(i18n("button.close"), ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().add(cancelBt);
return dialog.setupLayout();
}
The Main class is in charge of loading the warnings (stored in a .json file and deserialized upon starting the app). For now, the file only contains one entry.
When I click on my Warning button, the following function is called :
public void showWarnings() {
warningTable.getItems().clear(); // BP
warningTable.setItems(FXCollections.observableArrayList(warnings.values()));
warningDialog.showAndWait();
}
What happens is the following : When I have only one entry in my .json file, the first time I click on the button, only one warning is shown. If I click a second time, a second entry appears (the same) which should not be possible because of the following reasons :
Logic constraint : warnings.values() comes from an HashMap where the key is the type of the warning (WarningType class) > Not possible to have two identical keys
Debugging : When I set a breakpoint at "//BP", I clearly see that the warningTable has one item, and after clear the number of items is zero
Debugging : Still with the same breakpoint, I also check that warnings.values() has only one item, which is the case
After five clicks on the button, the Dialog clearly shows something is bugging.
More surprisingly, when I add a second warning (different from the first one, another type), the problem does not occur : No duplicates, warnings are correctly displayed and no matter how many times I open the window.
My question is : Could that be that the way I am creating this warning dialog leads to uncommon errors ? If so, why isn't it the case with two warnings ?
EDIT Include of the cellFactories / cellValueFactories
private void setupColumns() {
warnDescriptionCol.setCellFactory(new Callback<TableColumn<CustomWarning, String>, TableCell<CustomWarning, String>>() {
#Override
public TableCell<CustomWarning, String> call(TableColumn<CustomWarning, String> param) {
TableCell<CustomWarning, String> cell = new TableCell<CustomWarning, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
Label label = new Label(item);
setGraphic(label);
}
}
};
return cell;
}
});
warnDetailsCol.setCellFactory(new Callback<TableColumn<CustomWarning, CustomWarning>, TableCell<CustomWarning, CustomWarning>>() {
#Override
public TableCell<CustomWarning, CustomWarning> call(TableColumn<CustomWarning, CustomWarning> param) {
TableCell<CustomWarning, CustomWarning> cell = new TableCell<CustomWarning, CustomWarning>() {
#Override
protected void updateItem(CustomWarning item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
Button button = new Button(i18n("button.view"));
button.getStyleClass().add("save");
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
showWarning(item);
}
});
setGraphic(button);
}
}
};
return cell;
}
});
warnDescriptionCol.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<CustomWarning, String>, ObservableValue<String>>() {
TableViewObjectWrapper<CustomWarning, String> wrapper = new TableViewObjectWrapper<CustomWarning, String>() {
#Override
public String getData() {
return getModel().getTitle();
}
};
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<CustomWarning, String> param) {
return new ReadOnlyObjectWrapper<>(wrapper.setModel(param.getValue()).getData());
}
});
warnDetailsCol.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<CustomWarning, CustomWarning>, ObservableValue<CustomWarning>>() {
TableViewObjectWrapper<CustomWarning, CustomWarning> wrapper = new TableViewObjectWrapper<CustomWarning, CustomWarning>() {
#Override
public CustomWarning getData() {
return getModel();
}
};
#Override
public ObservableValue<CustomWarning> call(TableColumn.CellDataFeatures<CustomWarning, CustomWarning> param) {
return new ReadOnlyObjectWrapper<>(wrapper.setModel(param.getValue()).getData());
}
});
}
You have to clear your cells in the cell factory if the cell is empty, as explained in the documentation:
It is very important that subclasses of Cell override the updateItem method properly, as failure to do so will lead to issues such as blank cells or cells with unexpected content appearing within them. Here is an example of how to properly override the updateItem method:
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else {
setText(item.toString());
}
}
Note in this code sample two important points:
We call the super.updateItem(T, boolean) method. If this is not done, the item and empty properties are not correctly set, and you are likely to end up with graphical issues.
We test for the empty condition, and if true, we set the text and graphic properties to null. If we do not do this, it is almost guaranteed that end users will see graphical artifacts in cells unexpectedly.
Since the cells are reused, you have to clear the graphic if it has become empty, not just set it if it's not.
Related
We are using Xam.Plugin.HtmlLabel plugin in our xamarin forms application. When we set the html string in constructor, the html label is loading correctly in ios. But we assign the same label in method and called the method in constructor it is not loading in iOS and in android it's working fine. The issue reproduced code snippet is mentioned below. This issue is occurred only in iOS.
HTMLPage.xaml
<ContentPage.Content>
<Grid>
<ScrollView
Padding="24"
HorizontalScrollBarVisibility="Never"
VerticalScrollBarVisibility="Never">
<htmlLabel:HtmlLabel
Text="{Binding HTMLDescription}"
LinkColor="{StaticResource LinkColor}"
TextColor="{StaticResource PriTextColor}" />
</ScrollView>
</Grid>
</ContentPage.Content>
HTMLPage.xaml.cs
private string htmlDescription;
public string HTMLDescription
{
get { return htmlDescription; }
set { htmlDescription = value; OnPropertyChanged(); }
}
public HTMLPage(string Description)
{
InitializeComponent();
GetHTML(Description);
BindingContext = this;
}
private async void GetHTML(string description)
{
await Task.Delay(2000); //This delay is for getting the data from server.
HTMLDescription = description;
}
In whatever code does new HTMLPage("this is some html"), are you on the UI thread?
If not on UI thread, then that's your problem - dealing with UI elements off the UI thread is problematic.
If running on UI thread, then you have a different problem: a constructor is a "blocking" operation - it does no good to have async/await on code called inside a constructor; UI thread is blocked until the constructor returns! In general, its a bad idea to do anything lengthy there. Worst case, web query might delay until timeout.
Instead, try setting HTMLDescription AFTER page has appeared:
// Hold it until used.
string Description;
public HTMLPage(string description)
{
this.Description = description;
...
}
protected override void OnAppearing()
{
base.OnAppearing();
// Move to background, so OnAppearing can return.
Task.Run(() => {
// Potentially long operation.
var html = GetHTML(Description);
// Move to UI thread, before touching any UI element.
Device.BeginInvokeOnMainThread(() => {
HTMLDescription = html;
}
}
}
private string GetHTML(string description)
{
Task.Delay(2000); //This delay is for getting the data from server.
return description;
}
NOTE: I've removed async/await from this version of GetHTML, because it is only called on a background thread. You can put those back in, if desired.
Of course the downside of this, is that the page appears at first without that label. If you don't want that, then you need to instead GetHTML(...) BEFORE calling the constructor.
So your code (in a place you don't show) would be something like this:
var html = GetHTML(...);
new HTMLPage(html);
Thus, you are back to your original case that works, where in the constructor you already have the html string, so can simply do
HTMLDescription = description;
Which begs the question: why didn't you do that in the first place? Why did you put the call to GetHtML inside the constructor?
I have been working on an app that requires me to get information such as a Place Name and a PLace Photo from google PLaces Api and set it into a RecyclerView. I am stuck because I managed to get the code to work with no errors but the RecyclerView is empty. What is wrong with my code?
I am stuck because I don't know where the problem is. When I run the code, all the fetchs work and the tags show up in the Log so I am completely lost. My first thought is that I am displaying the code wrong but then I have no recourse to step forward and change it to something else because I am not sure if it would be better or worse.
This is the Fragment for the RecyclerView Item:
public class VenueList extends Fragment{
ArrayList<VenueItem> vIL = new ArrayList<>();
private PlacesClient placesClient;
private Context contextForPlaces;
place ids for the places I am currently using
String[] clubs = {"ChIJO_uSYKNZwokRAC7RLeB0oZ8", "ChIJAQBEylJYwokRLbnrAchQImk",
"ChIJU_26rfpYwokRTNf2K1-7p8E", "ChIJ38hxfnhZwokRx1HSFLj790w", "ChIJBwnlGrdZwokRpf61pMm860c"
, "ChIJpSIzqrhZwokR1KnVMoVty_g", "ChIJMRV7375ZwokRAfltF6Y-wYw", "ChIJYabdHPhYwokRPmAV8GtM3gs",
"ChIJi2dSjQRZwokRuXUKcv4riVc", "ChIJKaKVI79ZwokRN8WicODOIAw", "ChIJwXI8Fb5ZwokRr4JjG4HxSP8",
"ChIJ6bU_E4ZZwokR2ZDbY_IhhrI"};
#Override
public void onAttach(Context context) {
super.onAttach(context);
contextForPlaces = context;
}
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.menu_venue, container, false);
RecyclerView vRV = view.findViewById(R.id.view_venue);
List<Place.Field> placeFields = Arrays.asList(Place.Field.NAME, Place.Field.PHOTO_METADATAS);
if (!Places.isInitialized()) {
Places.initialize(contextForPlaces, "AIzaSyCKGd3fqmtsDklRGMhnkuIy1GS-j6gRBh8");}
placesClient = Places.createClient(contextForPlaces);
vRV.setHasFixedSize(true);
RecyclerView.LayoutManager vLM = new LinearLayoutManager(this.getActivity());
RecyclerView.Adapter vAdapter = new VenueAdapter(vIL);
// run through each photo to make sure it has a place attached to it then insert each photo and place into the vIL
//createBitmap for fetchPhoto
for (String club : clubs) {
FetchPlaceRequest request = FetchPlaceRequest.newInstance(club, placeFields);
placesClient.fetchPlace(request).addOnSuccessListener((response) -> {
Place place = response.getPlace();
PhotoMetadata photoMetadata = place.getPhotoMetadatas().get(0);
String attributions = photoMetadata.getAttributions();
FetchPhotoRequest photoRequest = FetchPhotoRequest.builder(photoMetadata).setMaxHeight(200).build();
placesClient.fetchPhoto(photoRequest).addOnSuccessListener((fetchPhotoResponse) -> {
Bitmap bitmap = fetchPhotoResponse.getBitmap();
vIL.add(new VenueItem(/*Photo*/bitmap, place/*Name*/));
Log.i(TAG, "Photo Should Be Up: ");
}).addOnFailureListener((exception) -> {
if (exception instanceof ApiException) {
ApiException apiException = (ApiException) exception;
int statusCode = apiException.getStatusCode();
// Handle error with given status code.
Log.e(TAG, "Place not found: " + exception.getMessage());
}
});
Log.i(TAG, "Place found: " + place.getName());
}).addOnFailureListener((exception) -> {
if (exception instanceof ApiException) {
ApiException apiException = (ApiException) exception;
int statusCode = apiException.getStatusCode();
// Handle error with given status code.
Log.e(TAG, "Place not found: " + exception.getMessage());
}
});
}
vRV.setLayoutManager(vLM);
vRV.setAdapter(vAdapter);
return view;
}
This is the part of the RecyclerView Adapter I changed. I used to be a getResourse for the image because the image was from the drawable folder
public void onBindViewHolder(#NonNull VenueViewHolder venueViewHolder, int i) {
VenueItem currentItem = vIAL.get(i);
if(currentItem.getVenueImageResource() == null){
venueViewHolder.vIV.setImageResource(R.drawable.ic_android);
}else
venueViewHolder.vIV.setImageBitmap(currentItem.getVenueImageResource());
venueViewHolder.vTV.setText((CharSequence) currentItem.getVenueDescription());
}
The Item itself which I also had to change a bit from its original. I made the string a Place and the int a Bitmap. I thought that would work.
public class VenueItem {
private Bitmap venueImageResource;
private Place venueDescription;
public VenueItem(Bitmap vIR, Place description) {
venueImageResource = vIR;
venueDescription = description;
}
public Bitmap getVenueImageResource() {
return venueImageResource;
}
public Place getVenueDescription() {
return venueDescription;
}
}
I want to be able to request a place name and a photo of the place using the placesClient and precent it in the for of a RecyclerView. I know the place Ids are correct because the Log returns the names of all the places. But they do not show up on the RecyclerView
I figured out the answer myself.
vRV.setLayoutManager(vLM);
vRV.setAdapter(vAdapter);
I just had to put these two lines into the for loop under the list item so that each item could escape before meing erased
Is it possible to use a simple action method - just like with Caliburn.Micro - instead of a command with MvvmCross bindings?
Example:
public void Action()
{
Tip = 11;
}
<Button
android:text="Button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="#+id/button1"
local:MvxBind="Click Action" />
It doesn't work out of the box, I tested that.
While I found a lot of samples about adding new target bindings, I didn't find a single one about adding a new source binding.
UPDATE:
This works now out of the box with the Rio binding. To use it, add the MvvmCross MethodBinding NuGet package to the Android project.
Up until now, much of the emphasis for MvvmCross has been on allowing multi-platform target binding with the source remaining mainly 'vanilla' INotifyPropertyChanged.
There have been some deviation in terms of ViewModel structure - e.g.:
the MvxCommandCollection - http://slodge.blogspot.co.uk/2013/03/fixing-mvvm-commands-making-hot-tuna.html
some users using Fody - http://twincoders.com/blog/codigo-limpio-con-fody/
Recently, several new feature requests have also been logged in this area:
AutoCommands - I think this is what you are asking about here - https://github.com/slodge/MvvmCross/issues/301
Rio binding sources - https://github.com/slodge/MvvmCross/issues/299
Tibet binding - https://github.com/slodge/MvvmCross/issues/298
Because of these, I do expect more functionality to be exposed in this area in the future...
With that said, if you wanted to get this working today, then MvvmCross Binding is overrideable so you could fairly easily do it:
1. Implement an ICommand that invokes a MethodInfo using reflection (for completeness this should probably also use a parameter if available) - some kind of InvokeMethodCommand (code for this left to the reader!)
.
2. Implement an MyMethodSourceBinding class which wraps the InvokeMethodCommand - something like:
public class MyMethodSourceBinding : MvxSourceBinding
{
private readonly MethodInfo _methodInfo;
protected MyMethodSourceBinding(object source, MethodInfo methodInfo)
: base(source)
{
_methodInfo = _methodInfo;
}
public override void SetValue(object value)
{
// do nothing - not allowed
}
public override Type SourceType
{
get { return typeof(ICommand); }
}
public override bool TryGetValue(out object value)
{
value = new InvokeMethodCommand(source, _methodInfo);
return true;
}
}
3. Override MvvmCross's registered IMvxSourceBindingFactory with your own implementation that can detect when a method is present - sadly most of this is cut and paste coding today - it would be something like
public class MySourceBindingFactory
: IMvxSourceBindingFactory
{
private IMvxSourcePropertyPathParser _propertyPathParser;
private IMvxSourcePropertyPathParser SourcePropertyPathParser
{
get
{
if (_propertyPathParser == null)
{
_propertyPathParser = Mvx.Resolve<IMvxSourcePropertyPathParser>();
}
return _propertyPathParser;
}
}
public IMvxSourceBinding CreateBinding(object source, string combinedPropertyName)
{
var tokens = SourcePropertyPathParser.Parse(combinedPropertyName);
return CreateBinding(source, tokens);
}
public IMvxSourceBinding CreateBinding(object source, IList<MvxPropertyToken> tokens)
{
if (tokens == null || tokens.Count == 0)
{
throw new MvxException("empty token list passed to CreateBinding");
}
var currentToken = tokens[0];
if (tokens.Count == 1)
{
return CreateLeafBinding(source, currentToken);
}
else
{
var remainingTokens = tokens.Skip(1).ToList();
return CreateChainedBinding(source, currentToken, remainingTokens);
}
}
private static MvxChainedSourceBinding CreateChainedBinding(object source, MvxPropertyToken propertyToken,
List<MvxPropertyToken> remainingTokens)
{
if (propertyToken is MvxIndexerPropertyToken)
{
return new MvxIndexerChainedSourceBinding(source, (MvxIndexerPropertyToken) propertyToken,
remainingTokens);
}
else if (propertyToken is MvxPropertyNamePropertyToken)
{
return new MvxSimpleChainedSourceBinding(source, (MvxPropertyNamePropertyToken) propertyToken,
remainingTokens);
}
throw new MvxException("Unexpected property chaining - seen token type {0}",
propertyToken.GetType().FullName);
}
private static IMvxSourceBinding CreateLeafBinding(object source, MvxPropertyToken propertyToken)
{
if (propertyToken is MvxIndexerPropertyToken)
{
return new MvxIndexerLeafPropertyInfoSourceBinding(source, (MvxIndexerPropertyToken) propertyToken);
}
else if (propertyToken is MvxPropertyNamePropertyToken)
{
//**************************
// Special code is here
var propertyToken = (MvxPropertyNamePropertyToken) propertyToken;
if (source != null)
{
var method = source.GetType().GetMethod(propertyToken.PropertyName, BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);
if (method != null)
{
return new MyMethodSourceBinding(source, method);
}
}
return new MvxSimpleLeafPropertyInfoSourceBinding(source,
(MvxPropertyNamePropertyToken) propertyToken);
// Special code ends here
//**************************
}
else if (propertyToken is MvxEmptyPropertyToken)
{
return new MvxDirectToSourceBinding(source);
}
throw new MvxException("Unexpected property source - seen token type {0}", propertyToken.GetType().FullName);
}
}
4. Supply this source binding factory in your own custom binding builder - e.g.:
public class MyAndroidBindingBuilder
: MvxAndroidBindingBuilder
{
protected override IMvxSourceBindingFactory CreateSourceBindingFactory()
{
return new MvxSourceBindingFactory();
}
}
5. Supply this binding builder during your setup
public class Setup : MvxAndroidSetup
{
// ....
protected override MvxAndroidBindingBuilder CreateBindingBuilder()
{
return new MyAndroidBindingBuilder();
}
}
Note: This approach is only for advanced users right now... As suggested in the first part of this question, I do expect the code in this area to change quite a lot so you might also encounter some issues maintaining a fork in this area. (Indeed the code in this area has already changed quite significantly on the Tibet Binding branch within the GitHub repo!)
so I've been looking for this for a week now and reading though every problem similar but none seemed to ask the same problem as mine exactly(try reverse engineering other solution similar to what I want with no success.
explained caveman style: I'm trying to create list using Metadata.
I open with a multi dialog and select more than one mp3
I put the file in an ArrayList<File>
I loop though the files with an enhanced for loop and extract metadata using a media variable
The info for the metadata ( like "artist") is what i want to save in an ArrayList for example
the problem is that the listener only works way after the enhanced loop has finished which results in
ArrayList<String> having one object with nothing in it
here is a sample:
ArrayList<String> al;
String path;
public void open(){
files=chooser.showOpenMultipleDialog(new Stage());
for( File f:files){
path=f.getPath();
Media media = new Media("file:/"+path.replace("\\", "/").replace(" ", "%20"));
al= new ArrayList<String>();
media.getMetadata().addListener(new MapChangeListener<String, Object>() {
public void onChanged(Change<? extends String, ? extends Object> change) {
if (change.wasAdded()) {
if (change.getKey().equals("artist")) {
al.add((String) change.getValueAdded());
}
}
}
});
}//close for loop
//then i want to see the size of al like this
system.out.println(al.size());
//then it returns 1 no matter how much file i selected
//when i system out "al" i get an empty string
the other way to read a media source metadata with adding a listener is extract that information in the mediaplayer .setOnReady(); here is an example part of the java controller class
public class uiController implements Initializable {
#FXML private Label label;
#FXML private ListView<String> lv;
#FXML private AnchorPane root;
#FXML private Button button;
private ObservableList<String> ol= FXCollections.observableArrayList();
private List<File> selectedFiles;
private final Object obj= new Object();
#Override
public void initialize(URL url, ResourceBundle rb) {
assert button != null : "fx:id=\"button\" was not injected: check your FXML file 'ui.fxml'.";
assert label != null : "fx:id=\"label\" was not injected: check your FXML file 'ui.fxml'.";
assert lv != null : "fx:id=\"lv\" was not injected: check your FXML file 'ui.fxml'.";
assert root != null : "fx:id=\"root\" was not injected: check your FXML file 'ui.fxml'.";
// initialize your logic here: all #FXML variables will have been injected
lv.setItems(ol);
}
#FXML private void open(ActionEvent event) {
FileChooser.ExtensionFilter extention= new FileChooser.ExtensionFilter("Music Files", "*.mp3","*.m4a","*.aif","*.wav","*.m3u","*.m3u8");
FileChooser fc= new FileChooser();
fc.setInitialDirectory(new File(System.getenv("userprofile")));
fc.setTitle("Select File(s)");
fc.getExtensionFilters().add(extention);
selectedFiles =fc.showOpenMultipleDialog(root.getScene().getWindow());
if(selectedFiles != null &&!selectedFiles.isEmpty()){
listFiles();
}
}
/**
* Convert each fie selected to its URI
*/
private void listFiles(){
try {
for (File file : selectedFiles) {
readMetaData(file.toURI().toString());
synchronized(obj){
obj.wait(100);
}
}
} catch (InterruptedException ex) {
}
System.gc();
}
/**
* Read a Media source metadata
* Note: Sometimes the was unable to extract the metadata especially when
* i have selected large number of files reasons i don't known why
* #param mediaURI Media file URI
*/
private void readMetaData(String mediaURI){
final MediaPlayer mp= new MediaPlayer(new Media(mediaURI));
mp.setOnReady(new Runnable() {
#Override
public void run() {
String artistName=(String) mp.getMedia().getMetadata().get("artist");
ol.add(artistName);
synchronized(obj){//this is required since mp.setOnReady creates a new thread and our loopp in the main thread
obj.notify();// the loop has to wait unitl we are able to get the media metadata thats why use .wait() and .notify() to synce the two threads(main thread and MediaPlayer thread)
}
}
});
}
}
the few changes that have made is used an ObservableList to store the artist name from the metadata
in the code you will find this
synchronized(obj){
obj.wait(100);
}
I do this because the mediaplayer .setOnReady() creates a new thread and the loop is in the main application thread, The loop has to wait for some time before the other thread is created and we are able to extract the metadata, and in the .setOnReady() there is a
synchronized(obj){
obj.notify;
}
to wake up the main thread hence the loop is able to move to the next item
I admit that this may not be the best solution to do this but am welcomed to anyone who has any better way on how to read JavaFx media metadata from a list of files
The full Netbeans project can be found here https://docs.google.com/file/d/0BxDEmOcXqnCLSTFHbTVFcGIzT1E/edit?usp=sharing
plus have created a small MediaPlayer Application using JavaFX which expolits use of the metadata https://docs.google.com/file/d/0BxDEmOcXqnCLR1Z0VGN4ZlJkbUU/edit?usp=sharing
You can use the following function to retrieve the metadata for a given Media object:
public static void initializeMetaData(Media media) {
final Ref<Boolean> ready = new Ref<>(false);
MediaPlayer mediaPlayer = new MediaPlayer(media);
mediaPlayer.setOnReady(() -> {
synchronized (ready) {
ready.set(false);
ready.notify();
}
});
synchronized (ready) {
if (!ready.get()) {
try {
ready.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
However, do not call initializeMetaData from a JavaFX thread, otherwise the thread runs into a deadlock.
PS: It's really ridiculous that one has to build such a workaround. I hope that in future Media will provide an initialize() method which does this job.
My solution to that issue was this:
public class MediaListener implements MapChangeListener<String, Object>
{
public String title = null;
public String artist = null;
public String album = null;
private final Consumer<MediaListener> handler;
private boolean handled = false;
public MediaListener(Consumer<MediaListener> handler)
{
this.handler = handler;
}
#Override
public void onChanged(MapChangeListener.Change<? extends String, ?> ch)
{
if (ch.wasAdded())
{
String key = ch.getKey();
switch (key)
{
case "title":
title = (String) ch.getValueAdded();
break;
case "artist":
artist = (String) ch.getValueAdded();
break;
case "album":
album = (String) ch.getValueAdded();
break;
}
if (!handled && title != null && artist != null && album != null)
{
handler.accept(this);
handled = true;
}
}
}
}
It may not be the best way but it's way cleaner than creating a new MediaPlayer per file.
Example usage:
Media media = Util.createMedia(path);
media.getMetadata().addListener(new MediaListener((data) ->
{
// Use the data object to access the media
}));
I have a frame which has a function that updates the frame when an event in another class is raised.
I have the class 'IRCClient' and 'MainFrame'. The IRCClient class has an event 'OnMessageRecvd', the MainFrame has a function 'HandleNewMessageReceived'. In the MainFrame class I have the variables 'CurrentServer' and 'CurrentChannel' to indicate what channel on what server is currently shown to the user.
Now, when I set the 'CurrentServer' and 'CurrentChannel' in the callback of a button, they have a value and all is fine. However, when the 'HandleNewMessageReceived' function is called by the 'OnMessageRecvd' event of IRCClient, the CurrentServer and CurrentChannel are both equal to any value (null) stated in the constructor of MainFrame.
Does anyone have an idea what the source of this behavior is? Thanks a lot in advance.
EDIT:
Below is the code, I've only posted the code in question (any function that uses the CurrentChannel and CurrentServer properties) and snipped away unrelated code.
// Main page, shows chat history.
public sealed partial class MainPage : LIRC.Common.LayoutAwarePage
{
private uint maxMessages;
IRCClient ircc;
IRCHistory irch;
string CurrentServer, CurrentChannel;
// Does all the setup for this class.
public MainPage()
{
this.InitializeComponent();
ircc = App.ircc; // This is a global variable in the 'App' class.
ircc.OnMessage += NewMessageReceived;
irch = App.irch; // This is also a global variable in the 'App' class.
currentChannel = currentServer = null;
}
// Restores the previous state.
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
if (pageState != null)
{
if(pageState.ContainsKey("viewedChannel"))
{
// Retrieve required info.
string[] viewedChannelTokens = (pageState["viewedChannel"] as string).Split('.');
CurrentChannel = viewedChannelTokens[0];
CurrentServer = viewedChannelTokens[1];
// If the saved channel or server got corrupt
if (string.IsNullOrEmpty(CurrentChannel) || string.IsNullOrEmpty(CurrentServer))
{
// Check if a channel is open, if so, select it.
*snip* // Non-relevant code.
}
// Clear and load required history.
ClearHistory();
if(CurrentServer != null && CurrentChannel != null)
LoadHistory(CurrentServer, CurrentChannel);
}
}
// Create buttons that switch to a channel
*Snip* // Calls AddChannelButton
}
// Creates a button that, when clicked, causes the ChatHistoryView to display the ChannelHistory.
void AddChannelButton(string Server, string Channel)
{
Button btn = new Button();
btn.Content = Channel + "\n" + Server;
btn.Width = 150;
// A function to switch to another channel.
btn.Click += (e, s) =>
{
ClearHistory(); // Clears the ChatHistoryVi.ew field.
LoadHistory(Server, Channel); // Does the actual loading of the channel history
CurrentChannel = Channel;
CurrentServer = Server;
};
ChannelBar.Children.Add(btn);
}
// The function that is called by the IRCClient.OnMessageRecv event.
public void NewMessageReceived(ref DataWriter dw, IRCServerInfo ircsi, IRCClient.RecvMessage recvmsg)
{
if (ircsi.Name == CurrentServer && CurrentChannel == recvmsg.recipient)
{
AddMessage(DateTimeToTime(DateTime.UtcNow), recvmsg.author, recvmsg.message);
}
}
}
// Responsible for creating, managing and closing connections.
public class IRCClient
{
// A structure that describes a message.
public struct RecvMessage
{
public string author; // Nickname
public string realName;
public string ipAddress;
public string recipient; // Indicates in what channel or private converstion.
public string message; // The actual message
};
// Describes how a function that handles a message should be declared.
public delegate void MessageHandler(ref DataWriter dw, IRCServerInfo ircsi, RecvMessage msg);
// Gets raised/called whenever a message was received.
public event MessageHandler OnMessage;
}
It's not clear what is happening from what you said, but if the variables are set to the values you set in the constructor when you check them - it means that either you have not changed them yet by the time you are expecting them to be changed or you set the value of some other variables instead of the ones you thought you had.
These are only guesses though and you can't expect more than guesses without showing your code.